//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.NonBlockingThread;


/* ------------------------------------------------------------ */
/** Provides a reusable BlockingCallback.
 * A typical usage pattern is:
 * <pre>
 * void someBlockingCall(Object... args) throws IOException
 * {
 *   try(Blocker blocker=sharedBlockingCallback.acquire())
 *   {
 *     someAsyncCall(args,blocker);
 *     blocker.block();
 *   }
 * }
 * </pre>
 */
public class SharedBlockingCallback
{
    static final Logger LOG = Log.getLogger(SharedBlockingCallback.class);

    final ReentrantLock _lock = new ReentrantLock();
    final Condition _idle = _lock.newCondition();
    final Condition _complete = _lock.newCondition();


    private static Throwable IDLE = new ConstantThrowable("IDLE");

    private static Throwable SUCCEEDED = new ConstantThrowable("SUCCEEDED");

    private static Throwable FAILED = new ConstantThrowable("FAILED");

    Blocker _blocker;
    
    public SharedBlockingCallback()
    {
        _blocker=new Blocker();
    }
    
    protected long getIdleTimeout()
    {
        return -1;
    }
    
    public Blocker acquire() throws IOException
    {
        _lock.lock();
        long idle = getIdleTimeout();
        try
        {
            while (_blocker._state != IDLE)
            {
                if (idle>0 && (idle < Long.MAX_VALUE/2))
                {
                    // Wait a little bit longer than the blocker might block
                    if (!_idle.await(idle*2,TimeUnit.MILLISECONDS))
                        throw new IOException(new TimeoutException());
                }
                else
                    _idle.await();
            }
            _blocker._state = null;
        }
        catch (final InterruptedException e)
        {
            throw new InterruptedIOException();
        }
        finally
        {
            _lock.unlock();
        }
        return _blocker;
    }

    protected void notComplete(Blocker blocker)
    {
        LOG.warn("Blocker not complete {}",blocker);
        if (LOG.isDebugEnabled())
            LOG.debug(new Throwable());
    }
    
    /* ------------------------------------------------------------ */
    /** A Closeable Callback.
     * Uses the auto close mechanism to check block has been called OK.
     */
    public class Blocker implements Callback, Closeable
    {
        Throwable _state = IDLE;
        
        protected Blocker()
        {
        }

        @Override
        public void succeeded()
        {
            _lock.lock();
            try
            {
                if (_state == null)
                {
                    _state = SUCCEEDED;
                    _complete.signalAll();
                }
                else
                    throw new IllegalStateException(_state);
            }
            finally
            {
                _lock.unlock();
            }
        }

        @Override
        public void failed(Throwable cause)
        {
            _lock.lock();
            try
            {
                if (_state == null)
                {
                    if (cause==null)
                        _state=FAILED;
                    else if (cause instanceof BlockerTimeoutException)
                        // Not this blockers timeout
                        _state=new IOException(cause);
                    else 
                        _state=cause;
                    _complete.signalAll();
                }
                else 
                    throw new IllegalStateException(_state);
            }
            finally
            {
                _lock.unlock();
            }
        }

        /**
         * Block until the Callback has succeeded or failed and after the return leave in the state to allow reuse. This is useful for code that wants to
         * repeatable use a FutureCallback to convert an asynchronous API to a blocking API.
         * 
         * @throws IOException
         *             if exception was caught during blocking, or callback was cancelled
         */
        public void block() throws IOException
        {
            if (NonBlockingThread.isNonBlockingThread())
                LOG.warn("Blocking a NonBlockingThread: ",new Throwable());
            
            _lock.lock();
            long idle = getIdleTimeout();
            try
            {
                while (_state == null)
                {
                    if (idle>0 && (idle < Long.MAX_VALUE/2))
                    {
                        // Wait a little bit longer than expected callback idle timeout
                        if (!_complete.await(idle+idle/2,TimeUnit.MILLISECONDS))
                            // The callback has not arrived in sufficient time.
                            // We will synthesize a TimeoutException 
                            _state=new BlockerTimeoutException();
                    }
                    else
                        _complete.await();
                }

                if (_state == SUCCEEDED)
                    return;
                if (_state == IDLE)
                    throw new IllegalStateException("IDLE");
                if (_state instanceof IOException)
                    throw (IOException)_state;
                if (_state instanceof CancellationException)
                    throw (CancellationException)_state;
                if (_state instanceof RuntimeException)
                    throw (RuntimeException)_state;
                if (_state instanceof Error)
                    throw (Error)_state;
                throw new IOException(_state);
            }
            catch (final InterruptedException e)
            {
                throw new InterruptedIOException();
            }
            finally
            {
                _lock.unlock();
            }
        }
        
        /**
         * Check the Callback has succeeded or failed and after the return leave in the state to allow reuse.
         * 
         * @throws IOException
         *             if exception was caught during blocking, or callback was cancelled
         */
        @Override
        public void close() throws IOException
        {
            _lock.lock();
            try
            {
                if (_state == IDLE)
                    throw new IllegalStateException("IDLE");
                if (_state == null)
                    notComplete(this);
            }
            finally
            {
                try 
                {
                    // If the blocker timed itself out, remember the state
                    if (_state instanceof BlockerTimeoutException)
                        // and create a new Blocker
                        _blocker=new Blocker();
                    else
                        // else reuse Blocker
                        _state = IDLE;
                    _idle.signalAll();
                    _complete.signalAll();
                } 
                finally 
                {
                    _lock.unlock();
                }
            }
        }

        @Override
        public String toString()
        {
            _lock.lock();
            try
            {
                return String.format("%s@%x{%s}",Blocker.class.getSimpleName(),hashCode(),_state);
            }
            finally
            {
                _lock.unlock();
            }
        }
    }
    
    private static class BlockerTimeoutException extends TimeoutException
    { 
    }
}
