package com.atlassian.stash.internal.scm.git;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

/**
 * A synchronization aid like {@link java.util.concurrent.CountDownLatch} that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 * <p>
 * {@code CountUpDownLatch} works just like {@code CountDownLatch}, but adds the option of {@link #countUp()
 * incrementing} the count.
 *
 * @see java.util.concurrent.CountDownLatch
 **/
public class CountUpDownLatch {
    private final Sync sync;

    public CountUpDownLatch(int count) {
        checkArgument(count >= 0, "count must be >= 0");
        sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public boolean await(long timeout, TimeUnit timeUnit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, timeUnit.toNanos(timeout));
    }

    public void countDown() {
        sync.countDown();
    }

    public void countUp() {
        sync.countUp();
    }

    public int getCount() {
        return sync.getCount();
    }

    /**
     * An AQS which allows both increments and decrements of a value and allows an await operation (with timeout) on the
     * zero value being reached.
     */
    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int startCount) {
            setState(startCount);
        }

        /**
         * Decrement this sync. A countDown operation may lead to a zero value which a thread may be waiting on
         * so this must go through {@link #releaseShared(int)}
         */
        private void countDown() {
            releaseShared(1);
        }

        /**
         * Increments this sync through compare and set semantics. As the value is always > 0, incrementing can never
         * lead to a zero value so doesn't need to go through {@link #releaseShared(int)}}
         */
        private void countUp() {
            while (true) {
                int value = getCount();
                int newValue = value + 1;
                if (compareAndSetState(value, newValue)) {
                    return;
                }
            }
        }

        int getCount() {
            return getState();
        }

        /**
         * Determines if the value is the one being waited for.
         *
         * @param arg ignored but part of the contract
         * @return a negative value on failure (value is non-zero) or a positive value if
         *         acquisition in shared mode succeeded (value is zero) and subsequent shared-mode
         *         acquires might also succeed, in which case a subsequent waiting thread must check availability.
         *         Zero, although permitted by the contract of {@link AbstractQueuedSynchronizer#tryAcquireShared(int)},
         *         will never be returned from this method.
         */
        @Override
        protected int tryAcquireShared(int arg) {
            //Zero value yields countdown success (return > 0), all other values yield failure (return < 0)
            return getCount() == 0 ? 1 : -1;
        }

        /**
         * Decrement the counter through compare and set semantics
         * @param arg ignored but part of the contract
         * @return true if the value after update is zero
         */
        @Override
        protected boolean tryReleaseShared(int arg) {
            //Decrement the counter through compare and set semantics
            while (true) {
                int value = getCount();
                checkState(value > 0, "count is already 0");
                int newValue = value - 1;
                if (compareAndSetState(value, newValue)) {
                    return newValue == 0;
                }
            }
        }
    }
}
