//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// This file is a part of the 'coroutines' project.
// Copyright 2018 Elmar Sonnenschein, esoco GmbH, Flensburg, Germany
//
// 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 de.esoco.coroutine.step.nio;

import de.esoco.coroutine.Continuation;
import de.esoco.coroutine.CoroutineContext;
import de.esoco.coroutine.CoroutineException;
import de.esoco.coroutine.CoroutineStep;
import de.esoco.coroutine.Suspension;

import java.io.IOException;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannel;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.CompletionHandler;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;

import org.obrel.core.RelationType;
import org.obrel.core.RelationTypes;

import static org.obrel.core.RelationTypes.newType;


/********************************************************************
 * The base class for coroutine steps that perform communication through
 * instances of {@link AsynchronousChannel}. It contains the inner class {@link
 * ChannelCallback} that implements most of the {@link CompletionHandler}
 * interface needed for asynchonous channel communication. The actual channel
 * operation must be provided to it as an implementation of the function
 * interface {@link ChannelOperation}.
 *
 * <p>To simplify the generic declaration of subclasses both input and output
 * type are declared as {@link ByteBuffer}, where the returned value will be the
 * input value. Input buffers must be provided by the preceding step in a
 * coroutine and initialize it for the respective channel step implemenation.
 * For a reading step this means that the buffer must have an adequate capacity.
 * For a writing step it must contain the data to write and it must have been
 * flipped if necessary (see {@link Buffer#flip()} for details).</p>
 *
 * @author eso
 */
public abstract class AsynchronousChannelStep<I, O> extends CoroutineStep<I, O>
{
	//~ Static fields/initializers ---------------------------------------------

	/** Internal signal for the first operation after a connect. */
	static final int FIRST_OPERATION = -2;

	/**
	 * State: the {@link AsynchronousChannelGroup} to associate any new
	 * asynchronous channels with.
	 */
	public static final RelationType<AsynchronousChannelGroup> CHANNEL_GROUP =
		newType();

	static
	{
		RelationTypes.init(AsynchronousChannelStep.class);
	}

	//~ Methods ----------------------------------------------------------------

	/***************************************
	 * Returns the {@link AsynchronousChannelGroup} for asynchronous channel
	 * operations in the current scope. If no such group exists a new one will
	 * be created with the {@link ExecutorService} of the {@link
	 * CoroutineContext} and stored as {@link #CHANNEL_GROUP} in the current
	 * scope.
	 *
	 * @param  rContinuation The channel group
	 *
	 * @return The channel group
	 */
	protected AsynchronousChannelGroup getChannelGroup(
		Continuation<?> rContinuation)
	{
		AsynchronousChannelGroup rChannelGroup =
			rContinuation.getState(CHANNEL_GROUP);

		if (rChannelGroup == null)
		{
			Executor rContextExecutor = rContinuation.context().getExecutor();

			if (rContextExecutor instanceof ExecutorService)
			{
				try
				{
					rChannelGroup =
						AsynchronousChannelGroup.withThreadPool(
							(ExecutorService) rContextExecutor);
				}
				catch (IOException e)
				{
					throw new CoroutineException(e);
				}

				rContinuation.scope().set(CHANNEL_GROUP, rChannelGroup);
			}
		}

		return rChannelGroup;
	}

	//~ Inner Interfaces -------------------------------------------------------

	/********************************************************************
	 * A functional interface used as argument to {@link ChannelCallback}.
	 *
	 * @author eso
	 */
	@FunctionalInterface
	protected interface ChannelOperation<C extends AsynchronousChannel>
	{
		//~ Methods ------------------------------------------------------------

		/***************************************
		 * Performs an asnychronous channel operation if necessary or returns
		 * FALSE.
		 *
		 * @param  nBytesProcessed The number of bytes that have been processed
		 *                         by a previous invocation
		 * @param  rChannel        The channel to perform the operation on
		 * @param  rData           The byte buffer for the operation data
		 * @param  rCallback       The callback to be invoked (recursively) upon
		 *                         completion of the operation
		 *
		 * @return FALSE if a recursive asynchronous execution has been started,
		 *         TRUE if the operation is complete
		 *
		 * @throws Exception Any kind of exception may be thrown
		 */
		public boolean execute(int						   nBytesProcessed,
							   C						   rChannel,
							   ByteBuffer				   rData,
							   ChannelCallback<Integer, C> rCallback)
			throws Exception;
	}

	//~ Inner Classes ----------------------------------------------------------

	/********************************************************************
	 * A {@link CompletionHandler} implementation that performs an asynchronous
	 * channel operation and resumes a coroutine step afterwards
	 * (asynchronously).
	 *
	 * @author eso
	 */
	protected static class ChannelCallback<V, C extends AsynchronousChannel>
		implements CompletionHandler<V, ByteBuffer>
	{
		//~ Instance fields ----------------------------------------------------

		private final C						 rChannel;
		private final Suspension<ByteBuffer> rSuspension;
		private ChannelOperation<C>			 fOperation;

		//~ Constructors -------------------------------------------------------

		/***************************************
		 * Creates a new instance.
		 *
		 * @param rChannel    The channel to operate on
		 * @param rSuspension The suspension to be resumed when the operation is
		 *                    completed
		 * @param fOperation  The asychronous channel operation to perform
		 */
		protected ChannelCallback(C						 rChannel,
								  Suspension<ByteBuffer> rSuspension,
								  ChannelOperation<C>    fOperation)
		{
			this.rChannel    = rChannel;
			this.rSuspension = rSuspension;
			this.fOperation  = fOperation;
		}

		//~ Methods ------------------------------------------------------------

		/***************************************
		 * {@inheritDoc}
		 */
		@Override
		@SuppressWarnings("unchecked")
		public void completed(V rResult, ByteBuffer rData)
		{
			// first invocation from connect is Void, then read/write integers
			int nProcessed =
				rResult instanceof Integer ? ((Integer) rResult).intValue()
										   : FIRST_OPERATION;

			try
			{
				// required to force THIS to integer to have only one implementation
				// as the Java NIO API declares the connect stage callback as Void
				if (fOperation.execute(
						nProcessed,
						rChannel,
						rData,
						(ChannelCallback<Integer, C>) this))
				{
					rSuspension.resume(rData);
				}
			}
			catch (Exception e)
			{
				rSuspension.fail(e);
			}
		}

		/***************************************
		 * {@inheritDoc}
		 */
		@Override
		public void failed(Throwable eError, ByteBuffer rData)
		{
			rSuspension.fail(eError);
		}
	}
}