package org.multiverse.commitbarriers;

import org.multiverse.api.Txn;
import org.multiverse.api.exceptions.DeadTxnException;
import org.multiverse.api.exceptions.TodoException;
import org.multiverse.utils.StandardThreadFactory;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.String.format;

/**
 * A CommitBarrier is a blocking structure like the {@link java.util.concurrent.CyclicBarrier} but
 * tailored to work with transactions. Based on this functionality, it is possible to create
 * a 2-phase commit for example.
 *
 * @author Peter Veentjer.
 */
@SuppressWarnings({"OverlyComplexClass"})
public abstract class CommitBarrier {

    private static int corePoolSize = 5;
    private static boolean runAsDaemon = true;
    private final static ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(
            corePoolSize, new StandardThreadFactory(Thread.NORM_PRIORITY, runAsDaemon));

    private volatile ScheduledExecutorService executorService = EXECUTOR;
    protected final Lock lock;
    protected final Condition statusCondition;

    private volatile Status status;
    private volatile int numberWaiting = 0;

    //for all non final non volatile variables; they only should be accessed while the lock is acquired.
    private List<Runnable> onAbortTasks = new LinkedList<Runnable>();
    private List<Runnable> onCommitTasks = new LinkedList<Runnable>();

    /**
     * Creates a new CommitBarrier.
     *
     * @param status the initial status of the CommitBarrier.
     * @param fair   if waking up threads is going to be fair.
     * @throws NullPointerException if status is null.
     */
    public CommitBarrier(Status status, boolean fair) {
        if (status == null) {
            throw new NullPointerException();
        }
        this.status = status;
        this.lock = new ReentrantLock(fair);
        this.statusCondition = lock.newCondition();
    }

    protected final Status getStatus() {
        return status;
    }

    /**
     * Returns the number of Transactions that have prepared and are waiting to commit. Value eventually
     * becomes null after a commit or abort.
     *
     * @return the number of transactions prepared.
     */
    public final int getNumberWaiting() {
        return numberWaiting;
    }

    /**
     * Checks if this CommitBarrier is closed. This is the initial status of the barrier.
     *
     * @return true if closed, false otherwise.
     */
    public final boolean isClosed() {
        return status == Status.Closed;
    }

    /**
     * Checks if this CommitBarrier already is committed.
     *
     * @return true if committed, false otherwise.
     */
    public final boolean isCommitted() {
        return status == Status.Committed;
    }

    /**
     * Checks if this CommitBarrier already is aborted.
     *
     * @return true if aborted, false otherwise.
     */
    public final boolean isAborted() {
        return status == Status.Aborted;
    }

    /**
     * Only should be made when the lock is acquired.
     *
     * @return the List of onCommitTasks that needs to be executed (is allowed to be null).
     */
    protected final List<Runnable> signalCommit() {
        numberWaiting = 0;
        status = Status.Committed;
        statusCondition.signalAll();
        onAbortTasks = null;
        List<Runnable> result = onCommitTasks;
        onCommitTasks = new LinkedList<Runnable>();
        return result;
    }

    /**
     * Only should be made when the lock is acquired.
     *
     * @return the List of onAbortTasks that needs to be executed (is allowed to be null).
     */
    protected final List<Runnable> signalAborted() {
        numberWaiting = 0;
        status = Status.Aborted;
        statusCondition.signalAll();
        onCommitTasks = new LinkedList<Runnable>();
        List<Runnable> result = onAbortTasks;
        onAbortTasks = new LinkedList<Runnable>();
        return result;
    }

    /**
     * Aborts this CommitBarrier. If there are any prepared transactions that are waiting for this CommitBarrier
     * to complete, they are aborted as well.
     * <p/>
     * If the CommitBarrier already is aborted, this call is ignored.
     *
     * @throws CommitBarrierOpenException if this CommitBarrier already is committed.
     */
    public final void abort() {
        List<Runnable> postAbortTasks = null;

        lock.lock();
        try {
            switch (status) {
                case Closed:
                    postAbortTasks = signalAborted();
                    break;
                case Aborted:
                    return;
                case Committed:
                    String commitMsg = "Can't abort already committed CommitBarrier";
                    throw new CommitBarrierOpenException(commitMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }

        executeTasks(postAbortTasks);
    }

    /**
     * Executes the tasks. Can be called with a null argument.
     *
     * @param tasks the tasks to execute.
     */
    protected static void executeTasks(final List<Runnable> tasks) {
        if (tasks == null) {
            return;
        }

        for (Runnable task : tasks) {
            task.run();
        }
    }

    /**
     * Awaits for this barrier to open (commit or abort). This call doesn't influence the state of this
     * CommitBarrier.
     *
     * @throws InterruptedException if the calling thread is interrupted while waiting.
     */
    public final void awaitOpen() throws InterruptedException {
        if (status != Status.Closed) {
            return;
        }

        lock.lockInterruptibly();
        try {
            while (status == Status.Closed) {
                statusCondition.await();
            }
        } finally {
            lock.unlock();
        }

    }

    /**
     * Awaits for this barrier to open (commit or abort). This call doesn't influence the state of this
     * CommitBarrier.
     * <p/>
     * This call is not responsive to interrupts.
     */
    public final void awaitOpenUninterruptibly() {
        if (status == Status.Closed) {
            lock.lock();
            try {
                while (status == Status.Closed) {
                    statusCondition.awaitUninterruptibly();
                }
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * Waits for this barrier to open (abort or commit). This call doesn't influence the state of this
     * CommitBarrier.
     *
     * @param timeout the maximum amount of time to wait for the barrier to close.
     * @param unit    the TimeUnit for the timeout argument.
     * @return true if the wait was a success, false if the barrier still is closed.
     * @throws InterruptedException if the thread is interrupted while waiting.
     * @throws NullPointerException if unit is null.
     */
    public final boolean tryAwaitOpen(final long timeout, final TimeUnit unit) throws InterruptedException {
        if (unit == null) {
            throw new NullPointerException();
        }

        if (status == Status.Closed) {
            long timeoutNs = unit.toNanos(timeout);

            lock.lockInterruptibly();
            try {
                while (status == Status.Closed) {
                    timeoutNs = statusCondition.awaitNanos(timeoutNs);
                    if (timeoutNs <= 0) {
                        return false;
                    }
                }
            } finally {
                lock.unlock();
            }
        }

        return true;
    }

    /**
     * Tries to await the close of the barrier. This call doesn't influence the state of this
     * CommitBarrier.
     * <p/>
     * This call is not responsive to interrupts.
     *
     * @param timeout the maximum amount of time to wait for the barrier to be closed.
     * @param unit    the timeunit for the timeout argument.
     * @return true if the wait was a success, false otherwise.
     */
    public final boolean tryAwaitOpenUninterruptibly(final long timeout, final TimeUnit unit) {
        if (unit == null) {
            throw new NullPointerException();
        }

        if (status == Status.Closed) {
            long timeoutNs = unit.toNanos(timeout);
            lock.lock();
            try {
                while (status == Status.Closed) {
                    timeoutNs = awaitNanosUninterruptible(timeoutNs);
                    if (timeoutNs <= 0) {
                        return false;
                    }
                }
            } finally {
                lock.unlock();
            }
        }

        return true;
    }

    private long awaitNanosUninterruptible(long timeoutNs) {
        boolean restoreInterrupt = Thread.interrupted();

        try {
            while (true) {
                long startNs = System.nanoTime();
                try {
                    return statusCondition.awaitNanos(timeoutNs);
                } catch (InterruptedException ex) {
                    timeoutNs -= (System.nanoTime() - startNs);
                    restoreInterrupt = true;
                }
            }
        } finally {
            //restore interrupt if needed
            if (restoreInterrupt) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Sets the ScheduledExecutorService to be used by this CommitBarrier for the timeout. This method can always
     * be called no matter the state of the CommitBarrier.
     *
     * @param executorService the ScheduledExecutorService this CommitBarrier is going to use for timeout.
     * @throws NullPointerException if executorService  is null.
     */
    public void setScheduledExecutorService(ScheduledExecutorService executorService) {
        if (executorService == null) {
            throw new NullPointerException();
        }
        this.executorService = executorService;
    }

    /**
     * Sets the timeout on this CommitBarrier. If the barrier hasn't committed/aborted before the timeout
     * it automatically is aborted. This is a function that typically is used when initializing the CommitBarrier.
     * <p/>
     * The timeout starts running when this method is called.
     *
     * @param timeout the maximum amount of time this barrier is allowed to run.
     * @param unit    the TimeUnit of the timeout parameter.
     * @throws NullPointerException       if unit is null.
     * @throws CommitBarrierOpenException if the CommitBarrier already is aborted or committed.
     */
    public final void setTimeout(final long timeout, final TimeUnit unit) {
        lock.lock();
        try {
            switch (status) {
                case Closed:
                    Runnable command = new Runnable() {
                        @Override
                        public void run() {
                            try {
                                abort();
                            } catch (IllegalStateException ignore) {
                            }
                        }
                    };
                    executorService.schedule(command, timeout, unit);
                    break;
                case Committed:
                    String commitMsg = "Can't set a timeout on an already commit CommitBarrier.";
                    throw new CommitBarrierOpenException(commitMsg);
                case Aborted:
                    String abortMsg = "Can't set a timeout on an already aborted CommitBarrier.";
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Registers a task that is executed once the CommitBarrier aborts.
     * <p/>
     * The task will be executed after the abort and will be executed by the thread that does the actual abort.
     * <p/>
     * The tasks will be executed in the order they are registered and will be executed at most once. If one of the
     * tasks throws a RuntimeException, the following will not be executed.
     *
     * @param task the task that is executed once the CommitBarrier commits.
     * @throws NullPointerException       if task is null.
     * @throws CommitBarrierOpenException if this CommitBarrier already is aborted or committed.
     */
    public final void registerOnAbortTask(final Runnable task) {
        lock.lock();
        try {
            switch (status) {
                case Closed:
                    if (task == null) {
                        throw new NullPointerException();
                    }

                    if (onAbortTasks == null) {
                        onAbortTasks = new LinkedList<Runnable>();
                    }

                    onAbortTasks.add(task);
                    break;
                case Committed:
                    String commitMsg = "Can't register on abort task on already committed CommitBarrier";
                    throw new CommitBarrierOpenException(commitMsg);
                case Aborted:
                    String abortMsg = "Can't register on abort task on already aborted CommitBarrier";
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Registers a task that is executed once the CommitBarrier commits.
     * <p/>
     * The task will be executed after the commit and will be executed by the thread that does the actual commit.
     * <p/>
     * The tasks will be executed in the order they are registered and will be executed at most once. If one of the
     * tasks throws a RuntimeException, the following will not be executed.
     *
     * @param task the task that is executed once the CommitBarrier commits.
     * @throws NullPointerException       if task is null.
     * @throws CommitBarrierOpenException if this CommitBarrier already is aborted or committed.
     */
    public final void registerOnCommitTask(final Runnable task) {
        lock.lock();
        try {
            switch (status) {
                case Closed:
                    if (task == null) {
                        throw new NullPointerException();
                    }

                    if (onCommitTasks == null) {
                        onCommitTasks = new LinkedList<Runnable>();
                    }

                    onCommitTasks.add(task);
                    break;
                case Committed:
                    String commitMsg = "Can't register on commit task on already committed CommitBarrier";
                    throw new CommitBarrierOpenException(commitMsg);
                case Aborted:
                    String abortMsg = "Can't register on commit task on already aborted CommitBarrier";
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Adds a waiters.
     * <p/>
     * Should only be called when the main lock is acquired.
     *
     * @throws IllegalStateException if the transaction isn't closed.
     */
    protected final void addJoiner() {
        if (status != Status.Closed) {
            throw new IllegalStateException();
        }

        numberWaiting++;
    }

    /**
     * Finishes a Txn.
     * <p/>
     * Can be called without the mainlock is acquired.
     *
     * @param tx the transaction to finish
     */
    protected final void finish(final Txn tx) {
        if (tx == null) {
            return;
        }

        if (isCommitted()) {
            tx.commit();
        } else if (isAborted()) {
            tx.abort();
            throw new IllegalStateException(
                    format("[%s] Didn't expect to encounter an aborted CommitBarrier",
                            tx.getConfig().getFamilyName()));
        }
    }

    /**
     * Ensures that a transaction is not dead.
     * <p/>
     * Can be called without the mainlock is acquired.
     *
     * @param tx        the transaction to check.
     * @param operation the name of the operation to checks if this transaction is not dead. Needed to provide
     *                  a useful message.
     * @throws org.multiverse.api.exceptions.DeadTxnException if tx is dead.
     * @throws NullPointerException     if tx is null.
     */
    protected static void ensureNotDead(final Txn tx, final String operation) {
        if (tx == null) {
            throw new NullPointerException();
        }

        if (!tx.getStatus().isAlive()) {
            throw new DeadTxnException(
                    format("[%s] Txn can't be used for %s since it isn't alive",
                            tx.getConfig().getFamilyName(), operation)
            );
        }
    }

    /**
     * Joins this CommitBarrier with the provided transaction. If the CommitBarrier can't commit yet, the method
     * will block.
     * <p/>
     * If the CommitBarrier already is aborted or committed, the transaction is aborted.
     * <p/>
     * This method is responsive to interrupts. If the waiting thread is interrupted, it will abort itself and
     * this CommitGroup.
     *
     * @param tx the Txn to commit.
     * @throws InterruptedException       if the thread is interrupted while waiting.
     * @throws NullPointerException       if tx is null.
     * @throws org.multiverse.api.exceptions.IllegalTxnStateException
     *                                    if the tx is no in the correct
     *                                    state for this operation.
     * @throws CommitBarrierOpenException if this VetoCommitBarrier is committed or aborted.
     */
    public void joinCommit(final Txn tx) throws InterruptedException {
        ensureNotDead(tx, "joinCommit");

        List<Runnable> tasks = null;

        lock.lock();
        try {
            switch (status) {
                case Closed:
                    tx.prepare();
                    addJoiner();
                    if (isLastParty()) {
                        tasks = signalCommit();
                    } else {
                        while (status == Status.Closed) {
                            try {
                                statusCondition.await();
                            } catch (InterruptedException ex) {
                                signalAborted();
                                tx.abort();
                                throw ex;
                            }
                        }
                    }
                    break;
                case Committed:
                    String committedMsg = format("Can't await commit on already committed VetoCommitBarrier " +
                            "with transaction %s", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(committedMsg);
                case Aborted:
                    String abortMsg = format("Can't await commit on already aborted VetoCommitBarrier " +
                            "with transaction %s", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }

        //todo: the the thread is interrupted, tx is not aborted.
        finish(tx);

        executeTasks(tasks);
    }

    /**
     * Joins this CommitBarrier with the provided transaction. If the CommitBarrier can't commit yet, this
     * method will block without being interruptible.
     * <p/>
     * If the CommitBarrier already is aborted or committed, the transaction is aborted.
     *
     * @param tx the Txn to join in the commit.
     * @throws NullPointerException       if tx is null.
     * @throws org.multiverse.api.exceptions.IllegalTxnStateException
     *                                    if the tx is not in the correct
     *                                    state for the operation.
     * @throws CommitBarrierOpenException if this VetoCommitBarrier is committed or aborted.
     */
    public void joinCommitUninterruptibly(final Txn tx) {
        ensureNotDead(tx, "joinCommitUninterruptibly");

        List<Runnable> postCommitTasks = null;
        lock.lock();
        try {
            switch (status) {
                case Closed:
                    tx.prepare();
                    addJoiner();

                    if (isLastParty()) {
                        postCommitTasks = signalCommit();
                    } else {
                        while (status == Status.Closed) {
                            statusCondition.awaitUninterruptibly();
                        }
                    }
                    break;
                case Aborted:
                    tx.abort();

                    String abortedMsg = format("Can't call joinCommitUninterruptible on already aborted " +
                            "CountDownCommitBarrier with transaction %s ", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(abortedMsg);
                case Committed:
                    tx.abort();

                    String commitMsg = format("Can't call joinCommitUninterruptible on already committed " +
                            "CountDownCommitBarrier with transaction %s ", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(commitMsg);
                default:
                    throw new IllegalStateException();
            }
        } finally {
            lock.unlock();
        }

        finish(tx);
        executeTasks(postCommitTasks);
    }

    /**
     * Tries to joins this CommitBarrier with the provided transaction. If the CommitBarrier can't commit yet, the
     * transaction and CommitBarrier will be aborted. So this method will not block (for a long period).
     * <p/>
     * If the CommitBarrier already is aborted or committed, the transaction is aborted.
     *
     * @param tx the Txn that wants to join the other parties to commit with.
     * @return true if CountDownCommitBarrier was committed, false if aborted.
     * @throws CommitBarrierOpenException if tx or this CountDownCommitBarrier is aborted or committed.
     * @throws NullPointerException       if tx is null.
     */
    public boolean tryJoinCommit(final Txn tx) {
        ensureNotDead(tx, "tryJoinCommit");

        List<Runnable> postCommitTasks = null;
        boolean abort = true;
        lock.lock();
        try {
            switch (status) {
                case Closed:
                    tx.prepare();
                    addJoiner();

                    if (isLastParty()) {
                        postCommitTasks = signalCommit();
                        abort = false;
                    } else {
                        postCommitTasks = signalAborted();
                    }
                    break;
                case Aborted:
                    String abortMsg = format("[%s] Can't call tryJoinCommit on already aborted " +
                            "CountDownCommitBarrier", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(abortMsg);
                case Committed:
                    String commitMsg = format("[%s] Can't call tryJoinCommit on already committed " +
                            "CountDownCommitBarrier", tx.getConfig().getFamilyName());
                    throw new CommitBarrierOpenException(commitMsg);
                default:
                    throw new IllegalStateException();
            }

        } finally {
            lock.unlock();
            if (abort) {
                tx.abort();
            } else {
                tx.commit();
            }
        }

        executeTasks(postCommitTasks);
        return isCommitted();
    }

    /**
     * Tries to joins this CommitBarrier with the provided transaction. If the CommitBarrier can't commit yet, this call
     * will block until one of the following things happens:
     * <ol>
     * <li>the CommitBarrier is committed before timeing out: the transaction also is committed</li>
     * <li>the CommitBarrier is aborted before timeing out: the transaction also is aborted</li>
     * <li>the thread is interrupted: the transaction and commit barrier also is aborted</li>
     * <li>the thread times out: the transaction and commit barrier are aborted</li>
     * </ol>
     * <p/>
     * If the CommitBarrier already is aborted or committed, the transaction is aborted.
     * <p/>
     * This method is responsive to interrupts. If the waiting thread is interrupted, it will abort itself and
     * this CommitBarrier.
     *
     * @param tx      the Txn that wants to join the other parties to commit with.
     * @param timeout the maximum time to wait.
     * @param unit    the TimeUnit for the timeout argument.
     * @return true if CountDownCommitBarrier was committed, false if aborted.
     * @throws CommitBarrierOpenException if tx or this CountDownCommitBarrier is aborted or committed.
     * @throws NullPointerException       if tx or unit is null is null.
     * @throws InterruptedException       if the calling thread is interrupted while waiting.
     */
    public boolean tryJoinCommit(final Txn tx, final long timeout, final TimeUnit unit) throws InterruptedException {
        ensureNotDead(tx, "tryJoinCommit");

        long timeoutNs = unit.toNanos(timeout);

        List<Runnable> postCommitTasks = null;

        lock.lock();
        try {
            switch (status) {
                case Closed:
                    tx.prepare();
                    addJoiner();
                    if (isLastParty()) {
                        postCommitTasks = signalCommit();
                    } else {
                        while (status == Status.Closed) {
                            try {
                                timeoutNs = statusCondition.awaitNanos(timeoutNs);
                                if (timeoutNs <= 0) {
                                    signalAborted();
                                    tx.abort();
                                    return false;
                                }
                            } catch (InterruptedException ex) {
                                signalAborted();
                                tx.abort();
                                //for the time being.. needs to be replaced with a really uninterruptible version
                                throw ex;
                            }
                        }
                    }

                    break;
                case Committed:
                    String commitMsg = "Can't await commit on an already committed VetoCommitBarrier";
                    throw new CommitBarrierOpenException(commitMsg);
                case Aborted:
                    String abortMsg = "Can't await commit on an already aborted VetoCommitBarrier";
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new NullPointerException();
            }
        } finally {
            lock.unlock();
        }

        finish(tx);
        executeTasks(postCommitTasks);
        return true;
    }

    /**
     * Tries to joins this CommitBarrier with the provided transaction. If the CommitBarrier can't commit yet, this call
     * will block until one of the following things happens:
     * <ol>
     * <li>the CommitBarrier is committed</li>
     * <li>the CommitBarrier is aborted</li>
     * </ol>
     * <p/>
     * If the CommitBarrier already is aborted or committed, the transaction is aborted.
     * <p/>
     * This method is not responsive to interrupts.
     *
     * @param tx      the Txn that wants to join the other parties to commit with.
     * @param timeout the maximum time to wait.
     * @param unit    the TimeUnit for the timeout argument.
     * @return true if CountDownCommitBarrier was committed, false if aborted.
     * @throws CommitBarrierOpenException if tx or this CountDownCommitBarrier is aborted or committed.
     * @throws NullPointerException       if tx or unit is null is null.
     */
    public boolean tryJoinCommitUninterruptibly(final Txn tx, final long timeout, final TimeUnit unit) {
        ensureNotDead(tx, "tryJoinCommitUninterruptibly");

        long timeoutNs = unit.toNanos(timeout);

        lock.lock();
        try {
            switch (status) {
                case Closed:
                    tx.prepare();
                    addJoiner();
                    while (status == Status.Closed) {
                        try {
                            timeoutNs = statusCondition.awaitNanos(timeoutNs);
                            if (timeoutNs <= 0) {
                                signalAborted();
                                tx.abort();
                                return false;
                            }
                        } catch (InterruptedException ex) {
                            signalAborted();
                            tx.abort();
                            //for the time being.. needs to be replaced with a really uninterruptible version
                            throw new RuntimeException(ex);
                        }
                    }
                    break;
                case Committed:
                    String commitMsg = "Can't await commit on an already committed VetoCommitBarrier";
                    throw new CommitBarrierOpenException(commitMsg);
                case Aborted:
                    String abortMsg = "Can't await commit on an already aborted VetoCommitBarrier";
                    throw new CommitBarrierOpenException(abortMsg);
                default:
                    throw new NullPointerException();
            }
        } finally {
            lock.unlock();
        }

        finish(tx);

        throw new TodoException();
    }

    protected abstract boolean isLastParty();

    enum Status {
        Closed, Committed, Aborted
    }
}
