package aQute.remote.util;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import aQute.lib.json.JSONCodec;

/**
 * This is a simple RPC module that has a R and L interface. The R interface is
 * implemented on the remote side. The methods on this subclass are then
 * available remotely. I.e. this is a two way street. Void messages are
 * asynchronous, other messages block to a reply.
 * 
 * @param <R>
 */
public class Link<L, R> extends Thread implements Closeable {
	private static final String[]		EMPTY		= new String[] {};
	static JSONCodec					codec		= new JSONCodec();

	final DataInputStream				in;
	final DataOutputStream				out;
	final Class<R>						remoteClass;
	final AtomicInteger					id			= new AtomicInteger(10000);
	final ConcurrentMap<Integer,Result>	promises	= new ConcurrentHashMap<Integer,Result>();
	final AtomicBoolean					quit		= new AtomicBoolean(false);
	volatile boolean					transfer	= false;
	private ThreadLocal<Integer>		msgid		= new ThreadLocal<Integer>();

	R									remote;
	L									local;
	ExecutorService						executor	= Executors.newFixedThreadPool(4);

	static class Result {
		boolean			resolved;
		byte[]			value;
		public boolean	exception;
	}

	public Link(Class<R> remoteType, L local, InputStream in, OutputStream out) {
		this(remoteType, local, new DataInputStream(in), new DataOutputStream(out));
	}

	@SuppressWarnings("unchecked")
	public Link(Class<R> remoteType, L local, DataInputStream in, DataOutputStream out) {
		super("link::" + remoteType.getName());
		setDaemon(true);
		this.remoteClass = remoteType;
		this.local = local == null ? (L) this : local;
		this.in = new DataInputStream(in);
		this.out = new DataOutputStream(out);
	}

	public Link(Class<R> type, L local, Socket socket) throws IOException {
		this(type, local, socket.getInputStream(), socket.getOutputStream());
	}

	public void open() {
		if (isAlive())
			throw new IllegalStateException("Already running");

		if (in != null)
			start();
	}

	public void close() throws IOException {
		if (quit.getAndSet(true) == true)
			return; // already closed

		if (local instanceof Closeable)
			try {
				((Closeable) local).close();
			} catch (Exception e) {}

		if (!transfer) {
			if (in != null)
				try {
					in.close();
				} catch (Exception e) {}
			if (out != null)
				try {
					out.close();
				} catch (Exception e) {}
		}
		executor.shutdownNow();
	}

	@SuppressWarnings("unchecked")
	public synchronized R getRemote() {
		if (quit.get())
			return null;

		if (remote == null)
			remote = (R) Proxy.newProxyInstance(remoteClass.getClassLoader(), new Class< ? >[] {
					remoteClass
			}, new InvocationHandler() {

				public Object invoke(Object target, Method method, Object[] args) throws Throwable {
					Object hash = new Object();

					try {
						if (method.getDeclaringClass() == Object.class)
							return method.invoke(hash, args);

						int msgId;
						try {
							msgId = send(id.getAndIncrement(), method, args);
							if (method.getReturnType() == void.class) {
								promises.remove(msgId);
								return null;
							}
						} catch (Exception e) {
							terminate(e);
							return null;
						}

						return waitForResult(msgId, method.getGenericReturnType());
					} catch (InvocationTargetException e) {
						Throwable t = e;
						while (t instanceof InvocationTargetException)
							t = ((InvocationTargetException) t).getTargetException();
						throw t;
					} catch (InterruptedException e) {
						interrupt();
						throw e;
					} catch (Exception e) {
						throw e;
					}
				}
			});
		return remote;
	}

	public void run() {
		while (!isInterrupted() && !transfer && !quit.get())
			try {
				final String cmd = in.readUTF();
				trace("rx " + cmd);
				final int id = in.readInt();

				int count = in.readShort();
				final List<byte[]> args = new ArrayList<byte[]>(count);
				for (int i = 0; i < count; i++) {
					int length = in.readInt();
					byte[] data = new byte[length];
					in.readFully(data);
					args.add(data);
				}

				Runnable r = new Runnable() {
					public void run() {
						try {
							msgid.set(id);
							executeCommand(cmd, id, args);
						} catch (Exception e) {
							// e.printStackTrace();
						}
						msgid.set(-1);
					}

				};
				executor.execute(r);
			} catch (SocketTimeoutException ee) {
				// Ignore, just to allow polling the actors again
			} catch (Exception ee) {

				terminate(ee);
				return;
			}
	}

	/*
	 * Signalling function /
	 */

	protected void terminate(Exception t) {
		try {
			close();
		} catch (IOException e) {}
	}

	Method getMethod(String cmd, int count) {

		for (Method m : local.getClass().getMethods()) {
			if (m.getDeclaringClass() == Link.class)
				continue;

			if (m.getName().equals(cmd) && m.getParameterTypes().length == count) {
				return m;
			}
		}
		return null;
	}

	int send(int msgId, Method m, Object args[]) throws Exception {
		if (m != null)
			promises.put(msgId, new Result());
		trace("send");
		synchronized (out) {
			out.writeUTF(m != null ? m.getName() : "");
			out.writeInt(msgId);
			if (args == null)
				args = EMPTY;

			out.writeShort(args.length);
			for (int i = 0; i < args.length; i++) {
				Object arg = args[i];

				if (arg instanceof byte[]) {
					byte[] data = (byte[]) arg;
					out.writeInt(data.length);
					out.write(data);
				} else {
					ByteArrayOutputStream bout = new ByteArrayOutputStream();
					codec.enc().to(bout).put(arg);
					byte[] data = bout.toByteArray();
					out.writeInt(data.length);
					out.write(data);
				}
			}
			out.flush();
			trace("sent");
		}
		return msgId;
	}

	void response(int msgId, byte[] data) {
		boolean exception = false;
		if (msgId < 0) {
			msgId = -msgId;
			exception = true;
		}

		Result o = promises.get(msgId);
		if (o != null) {
			synchronized (o) {
				trace("resolved");
				o.value = data;
				o.exception = exception;
				o.resolved = true;
				o.notifyAll();
			}
		}
	}

	@SuppressWarnings("unchecked")
	<T> T waitForResult(int id, Type type) throws Exception {
		long deadline = System.currentTimeMillis() + 300000;
		Result result = promises.get(id);

		try {
			do {
				synchronized (result) {
					if (result.resolved) {

						if (result.value == null)
							return null;

						if (result.exception) {
							String msg = codec.dec().from(result.value).get(String.class);
							System.out.println("Exception " + msg);
							throw new RuntimeException(msg);
						}

						if (type == byte[].class)
							return (T) result.value;

						T value = (T) codec.dec().from(result.value).get(type);
						return value;
					}

					long delay = deadline - System.currentTimeMillis();
					if (delay <= 0) {
						return null;
					}
					trace("start delay " + delay);
					result.wait(delay);
					trace("end delay " + (delay - (deadline - System.currentTimeMillis())));
				}
			} while (true);
		} finally {
			promises.remove(id);
		}
	}

	private void trace(String string) {
		// TODO Auto-generated method stub

	}

	/*
	 * Execute a command in a background thread
	 */

	void executeCommand(final String cmd, final int id, final List<byte[]> args) throws Exception {
		if (cmd.isEmpty())
			response(id, args.get(0));
		else {

			Method m = getMethod(cmd, args.size());
			if (m == null) {
				return;
			}

			Object parameters[] = new Object[args.size()];
			for (int i = 0; i < args.size(); i++) {
				Class< ? > type = m.getParameterTypes()[i];
				if (type == byte[].class)
					parameters[i] = args.get(i);
				else {
					parameters[i] = codec.dec().from(args.get(i)).get(m.getGenericParameterTypes()[i]);
				}
			}

			try {
				Object result = m.invoke(local, parameters);

				if (transfer || m.getReturnType() == void.class)
					return;

				try {
					send(id, null, new Object[] {
							result
					});
				} catch (Exception e) {
					terminate(e);
					return;
				}
			} catch (Throwable t) {
				while (t instanceof InvocationTargetException
						&& ((InvocationTargetException) t).getTargetException() != null)
					t = ((InvocationTargetException) t).getTargetException();
				// t.printStackTrace();
				try {
					send(-id, null, new Object[] {
							t + ""
					});
				} catch (Exception e) {
					terminate(e);
					return;
				}
			}
		}
	}

	public boolean isOpen() {
		return !quit.get();
	}

	public DataOutputStream getOutput() {
		assert transfer && !isOpen();
		return out;
	}

	public DataInputStream getInput() {
		assert transfer && !isOpen();
		return in;
	}

	@SuppressWarnings("unchecked")
	public void setRemote(Object remote) {
		this.remote = (R) remote;
	}

	public void transfer(Object result) throws Exception {
		transfer = true;
		quit.set(true);
		interrupt();
		join();
		if (result != null)
			send(msgid.get(), null, new Object[] {
					result
			});
		close();
	}

}
