package org.scribble.net.session;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.scribble.main.RuntimeScribbleException;
import org.scribble.net.ScribInterrupt;
import org.scribble.net.ScribMessage;

public abstract class BinaryChannelEndpoint
{
	//protected MPSTEndpoint<?, ?> se;
	protected SessionEndpoint<?, ?> se;
	
	//protected BinaryChannelEndpoint parent;
	private AbstractSelectableChannel c;
	private ByteBuffer bb;

	//private final SocketChannel;
	protected final List<ScribMessage> msgs = new LinkedList<>();
	
	private boolean isClosed = false;

	private int count = 0;  // How many ScribMessages read so far  // volatile?
	private int ticket = 0;  // Index of the next expected ScribMessage
	
	private final List<CompletableFuture<ScribMessage>> pending = new LinkedList<>();

	// Server side
	//protected BinaryChannelEndpoint(MPSTEndpoint<?, ?> se, AbstractSelectableChannel c) throws IOException
	protected BinaryChannelEndpoint(SessionEndpoint<?, ?> se, AbstractSelectableChannel c) throws IOException
	{
		this.bb = ByteBuffer.allocate(16921);  // FIXME: size  // Use put mode as default
		init(se, c);
	}

	// Client side
	protected BinaryChannelEndpoint()
	{
		this.bb = ByteBuffer.allocate(16921);  // FIXME: size  // Use put mode as default
	}
	
	//public abstract void initClient(MPSTEndpoint<?, ?> se, String host, int port) throws IOException;
	public abstract void initClient(SessionEndpoint<?, ?> se, String host, int port) throws IOException;
	
	//protected void init(MPSTEndpoint<?, ?> se, AbstractSelectableChannel c) throws IOException
	protected void init(SessionEndpoint<?, ?> se, AbstractSelectableChannel c) throws IOException
	{
		this.se = se;
		this.c = c;
		this.c.configureBlocking(false);
	}

	// Pre: selector is paused
	//protected BinaryChannelEndpoint(BinaryChannelEndpoint c)
	public void wrapChannel(BinaryChannelEndpoint c) throws IOException
	{
		this.se = c.se;
		//this.msgs.addAll(c.msgs);  // Guaranteed to be empty/0 for reconnect?
		//this.count = c.count;
		//this.ticket = c.ticket;
		//this.parent = c;
		this.c = c.c;
		this.bb = c.bb;
		
		//FIXME: complete all pending futures on parent chan -- no: not enough by itself, that is just reading the already-deserialized cache
		//FIXME: pull all pending data out of parent chan (due to selector not handling it yet -- in send states, we just need to clear all expected messages up to this point)
		//  -- so that wrapper handshake is starting clean
		// --- futures must be completed before here, since selector is paused
	}
	
	public AbstractSelectableChannel getSelectableChannel()  // For asynchrony (via nio Selector) -- maybe implement/extend instead
	{
		return this.c;
	}
	
	public void write(ScribMessage m) throws IOException
	{
		writeBytes(this.se.smf.toBytes(m));
	}

	// Default CompletableFuture executed by common forkjoin pool -- so all messages that are received/async'd will eventually be pulled from the queue (no manual GC necessary)
	public synchronized CompletableFuture<ScribMessage> getFuture()
	{
		// FIXME: better exception handling (integrate with Future interface?)
		final int ticket = getTicket();
		CompletableFuture<ScribMessage> fut = CompletableFuture.supplyAsync(() ->
				{
					try
					{
						ScribMessage m = read(ticket);
						if (m instanceof ScribInterrupt)  // FIXME: hacked in
						{
							throw new RuntimeScribbleException((Throwable) ((ScribInterrupt) m).payload[0]);
						}
						return m;
					}
					catch(IOException e)
					{
						throw new RuntimeScribbleException(e);
					}
					finally
					{
						this.pending.remove(0);  // Safe?
					}
				});
		synchronized (this.pending)
		{
			this.pending.add(fut);
		}
		return fut;
	}
	
	protected void sync() throws IOException  // Hacky
	{
		try
		{
			synchronized (this.pending)
			{
				if (!this.pending.isEmpty())
				{
					this.pending.get(this.pending.size() - 1).get();
				}
			}
		}
		catch (InterruptedException | ExecutionException e)
		{
			throw new IOException(e);
		}
	}
	
	private synchronized ScribMessage read(int ticket) throws IOException
	{
		try
		{
			while (this.count < ticket && !this.isClosed)  // "Chain" futures directly? i.e. each future syncs on predecessor?
			{
				wait();
			}
			while (this.msgs.isEmpty() && !this.isClosed)
			{
				wait();
			}
			if (this.isClosed)
			{
				throw new IOException("Channel closed");
			}
			this.count++;
			ScribMessage m = this.msgs.remove(0);
			notifyAll();  // A later future might have gone first and blocked, so wake up (with the message already enqueued, so no notify coming from there)  // finally?
			return m;
		}
		catch (InterruptedException e)
		{
			throw new IOException(e);
		}
	}
	
	protected synchronized void enqueue(ScribMessage m)
	{
		this.msgs.add(m);
		//this.count++;
		notifyAll();
	}

	public abstract void writeBytes(byte[] bs) throws IOException;
	protected abstract void readBytesIntoBuffer() throws IOException;  // synchronized (against read)  // bytes ready for reading: try to deserialize and then enqueue, or else cache for later
	
	public synchronized void readAndEnqueueMessages() throws ClassNotFoundException, IOException  // Here for synchronisation
	{
		readBytesIntoBuffer();
		ScribMessage m;
		while ((m = this.se.smf.fromBytes(this.bb)) != null)
		{
			enqueue(m);
		}
	}
	
	public synchronized void close() throws IOException
	{
		this.isClosed = true;
		notifyAll();
	}
	
	public synchronized int getTicket()
	{
		//return ++this.ticket;
		return this.ticket++;
	}
	
	// post: bb:put
	public ByteBuffer getBuffer()
	{
		return this.bb;
	}
}
