// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.ioc.internal.util;

import org.apache.tapestry5.ioc.Invokable;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * A barrier used to execute code in a context where it is guarded by read/write locks. In addition, handles upgrading
 * read locks to write locks (and vice versa). Execution of code within a lock is in terms of a {@link Runnable} object
 * (that returns no value), or a {@link Invokable} object (which does return a value).
 */
public class ConcurrentBarrier
{
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * This is, of course, a bit of a problem. We don't have an avenue for ensuring that this ThreadLocal is destroyed
     * at the end of the request, and that means a thread can hold a reference to the class and the class loader which
     * loaded it. This may cause redeployment problems (leaked classes and class loaders). Apparently JDK 1.6 provides
     * the APIs to check to see if the current thread has a read lock. So, we tend to remove the TL, rather than set its
     * value to false.
     */
    private static class ThreadBoolean extends ThreadLocal<Boolean>
    {
        @Override
        protected Boolean initialValue()
        {
            return false;
        }
    }

    private final ThreadBoolean threadHasReadLock = new ThreadBoolean();

    /**
     * Invokes the object after acquiring the read lock (if necessary). If invoked when the read lock has not yet been
     * acquired, then the lock is acquired for the duration of the call. If the lock has already been acquired, then the
     * status of the lock is not changed.
     *
     * TODO: Check to see if the write lock is acquired and <em>not</em> acquire the read lock in that situation.
     * Currently this code is not re-entrant. If a write lock is already acquired and the thread attempts to get the
     * read lock, then the thread will hang. For the moment, all the uses of ConcurrentBarrier are coded in such a way
     * that reentrant locks are not a problem.
     *
     * @param <T>
     * @param invokable
     * @return the result of invoking the invokable
     */
    public <T> T withRead(Invokable<T> invokable)
    {
        boolean readLockedAtEntry;

        synchronized (threadHasReadLock)
        {
            readLockedAtEntry = threadHasReadLock.get();
        }

        if (!readLockedAtEntry)
        {
            lock.readLock().lock();

            synchronized (threadHasReadLock)
            {
                threadHasReadLock.set(true);
            }
        }

        try
        {
            return invokable.invoke();
        }
        finally
        {
            if (!readLockedAtEntry)
            {
                lock.readLock().unlock();

                synchronized (threadHasReadLock)
                {
                    threadHasReadLock.remove();
                }
            }
        }
    }

    /**
     * As with {@link #withRead(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
     */
    public void withRead(final Runnable runnable)
    {
        Invokable<Void> invokable = new Invokable<Void>()
        {
            @Override
            public Void invoke()
            {
                runnable.run();

                return null;
            }
        };

        withRead(invokable);
    }

    /**
     * Acquires the exclusive write lock before invoking the Invokable. The code will be executed exclusively, no other
     * reader or writer threads will exist (they will be blocked waiting for the lock). If the current thread has a read
     * lock, it is released before attempting to acquire the write lock, and re-acquired after the write lock is
     * released. Note that in that short window, between releasing the read lock and acquiring the write lock, it is
     * entirely possible that some other thread will sneak in and do some work, so the {@link Invokable} object should
     * be prepared for cases where the state has changed slightly, despite holding the read lock. This usually manifests
     * as race conditions where either a) some parallel unrelated bit of work has occured or b) duplicate work has
     * occured. The latter is only problematic if the operation is very expensive.
     *
     * @param <T>
     * @param invokable
     */
    public <T> T withWrite(Invokable<T> invokable)
    {
        boolean readLockedAtEntry = releaseReadLock();

        lock.writeLock().lock();

        try
        {
            return invokable.invoke();
        }
        finally
        {
            lock.writeLock().unlock();
            restoreReadLock(readLockedAtEntry);
        }
    }

    private boolean releaseReadLock()
    {
        boolean readLockedAtEntry;

        synchronized (threadHasReadLock)
        {
            readLockedAtEntry = threadHasReadLock.get();
        }

        if (readLockedAtEntry)
        {
            lock.readLock().unlock();

            synchronized (threadHasReadLock)
            {
                threadHasReadLock.set(false);
            }
        }

        return readLockedAtEntry;
    }

    private void restoreReadLock(boolean readLockedAtEntry)
    {
        if (readLockedAtEntry)
        {
            lock.readLock().lock();

            synchronized (threadHasReadLock)
            {
                threadHasReadLock.set(true);
            }
        }
        else
        {
            synchronized (threadHasReadLock)
            {
                threadHasReadLock.remove();
            }
        }
    }

    /**
     * As with {@link #withWrite(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
     */
    public void withWrite(final Runnable runnable)
    {
        Invokable<Void> invokable = new Invokable<Void>()
        {
            @Override
            public Void invoke()
            {
                runnable.run();

                return null;
            }
        };

        withWrite(invokable);
    }

    /**
     * Try to aquire the exclusive write lock and invoke the Runnable. If the write lock is obtained within the specfied
     * timeout, then this method behaves as {@link #withWrite(Runnable)} and will return true. If the write lock is not
     * obtained within the timeout then the runnable is never invoked and the method will return false.
     *
     * @param runnable    Runnable object to execute inside the write lock.
     * @param timeout     Time to wait for write lock.
     * @param timeoutUnit Units of timeout.
     * @return true if lock was obtained and the runnable executed, or false otherwise.
     */
    public boolean tryWithWrite(final Runnable runnable, long timeout, TimeUnit timeoutUnit)
    {
        boolean readLockedAtEntry = releaseReadLock();

        boolean obtainedLock = false;

        try
        {
            try
            {
                obtainedLock = lock.writeLock().tryLock(timeout, timeoutUnit);

                if (obtainedLock) runnable.run();

            }
            catch (InterruptedException e)
            {
                obtainedLock = false;
            }
            finally
            {
                if (obtainedLock) lock.writeLock().unlock();
            }
        }
        finally
        {
            restoreReadLock(readLockedAtEntry);
        }

        return obtainedLock;
    }

}
