package com.eactive.eai.adapter.socket2.filter.executor;

import com.eactive.eai.adapter.socket2.config.SocketAdapterContext;
import com.eactive.eai.adapter.socket2.config.SocketLoggerManager;
import com.eactive.eai.adapter.socket2.protocol.ISocketAdapter;
import com.eactive.eai.common.util.Logger;

import edu.emory.mathcs.backport.java.util.concurrent.Executor;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoFilterAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.util.ByteBufferUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;


public class BoundedExecutorFilter extends IoFilterAdapter {
	private final Logger logger ;
	private final Executor executor;
	private final Semaphore semaphore;
	private ISocketAdapter adapter;
	private SocketAdapterContext context;

	
	// ----------------------------------------------------------------------
	// added by bmchae
	// ----------------------------------------------------------------------
	public BoundedExecutorFilter(ISocketAdapter adapter, Executor executor) {
		this.executor = executor;
		
		//-------------------------------------------------------
		// 2009.05.12
		// Connect retry Error
		// Outbound의 MaxThread 는 10으로 고정되고
		// connection이 20일 경우
		// 상대편의 Socket Server 가 재기동 되었을 경우 
		// retry 수행시 
		// this.semaphore 가 20 이 되므로 문제가 발생함
		//-------------------------------------------------------
//		this.semaphore = new Semaphore(adapter.getContext().getMaxThread() + 10);
		if(adapter.getContext().getMaxThread() > adapter.getContext().getMaxConnection()) {
			this.semaphore = new Semaphore(adapter.getContext().getMaxThread() + 10);
		}
		else {
			this.semaphore = new Semaphore(adapter.getContext().getMaxConnection() + 10);
		}
		this.adapter = adapter;
		this.context = adapter.getContext();
	    logger = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());
	}

	/**
	 * Returns the underlying {@link Executor} instance this filter uses.
	 */
	public Executor getExecutor() {
		return executor;
	}

	private void fireEvent(NextFilter nextFilter, IoSession session, EventType type, Object data) {
		Event event = new Event(type, nextFilter, data);
		final SessionBuffer sessionBuffer = SessionBuffer.getSessionBuffer(session);

		synchronized (sessionBuffer.eventQueue) {
			sessionBuffer.eventQueue.add(event);
		}
		
		if (sessionBuffer.processingCompleted) {
			sessionBuffer.processingCompleted = false;
			if (logger.isDebugEnabled()) {
				logger.debug("Launching thread for " + session.getRemoteAddress());
			}

			if (Thread.currentThread().getName().contains("IoProcessor")||Thread.currentThread().getName().contains("DatagramAcceptor")) {
				try {
					if (semaphore.tryAcquire(context.getTimeout(), TimeUnit.SECONDS)) {		
						executor.execute(new Runnable() {
							public void run() {
								try {
									(new ProcessEventsRunnable(context, sessionBuffer)).run();									
								} finally {				
									semaphore.release();
								}
							}
						});
					} else {
						throw new RuntimeException("timeout (" + context.getTimeout() + ") occurred when allocate thread");
					}
				} catch(InterruptedException e) {
					throw new RuntimeException("thread interrupted when acquire semaphore", e);
				} catch(RejectedExecutionException e) {
					semaphore.release();
					throw new RuntimeException("cannot allocate thread -> increase thread pool size (max.threads)", e);
				}
			} else {
				(new ProcessEventsRunnable(context, sessionBuffer)).run();	
			}
		} else { //if (type==EventType.CLOSED  || type==EventType.EXCEPTION) {
			//--------------------------------------------------------------
			// added by bmchae
			// client && sync && non-permanent 일 경우 CLOSED/EXCEPTION event 무시
			//--------------------------------------------------------------
			if (context.isClient() 
				&& context.isSync()
				&& !context.isPermanent() 
				//&& context.isBlocking()
			    && (type.equals(EventType.CLOSED) 
			       || (type.equals(EventType.EXCEPTION) && data instanceof java.io.IOException))) {				
				return;
			}

			if (Thread.currentThread().getName().contains("IoProcessor")||Thread.currentThread().getName().contains("DatagramAcceptor")) {
				try {
					if (semaphore.tryAcquire(context.getTimeout(), TimeUnit.SECONDS)) {
						executor.execute(new Runnable() {
							public void run() {
								try {
									(new ProcessEventsRunnable(context, sessionBuffer)).run();									
								} finally {
									semaphore.release();
								}
							}
						});
					} else {
						throw new RuntimeException("timeout (" + context.getTimeout() + ") occurred when allocate thread");
					}
				} catch(InterruptedException e) {
					throw new RuntimeException("thread interrupted when acquire semaphore", e);
				} catch(RejectedExecutionException e) {
					semaphore.release();
					throw new RuntimeException("cannot allocate thread -> increase thread pool size (max.threads)", e);
				}
			} else {
				(new ProcessEventsRunnable(context, sessionBuffer)).run();	
			}
		}
	}

	private static class SessionBuffer {
		private static final String KEY = SessionBuffer.class.getName() + ".KEY";

		private static SessionBuffer getSessionBuffer(IoSession session) {
			synchronized (session) {
				SessionBuffer sessionBuffer = (SessionBuffer) session.getAttribute(KEY);
				if (sessionBuffer == null) {
					sessionBuffer = new SessionBuffer(session);
					session.setAttribute(KEY, sessionBuffer);
				}
				return sessionBuffer;
			}
		}

		private final IoSession session;

		private final List eventQueue = new ArrayList();

		private boolean processingCompleted = true;

		private SessionBuffer(IoSession session) {
			this.session = session;
		}
	}

	protected static class EventType {
		public static final EventType OPENED = new EventType("OPENED");
		public static final EventType CLOSED = new EventType("CLOSED");
		public static final EventType READ = new EventType("READ");
		public static final EventType WRITTEN = new EventType("WRITTEN");
		public static final EventType RECEIVED = new EventType("RECEIVED");
		public static final EventType SENT = new EventType("SENT");
		public static final EventType IDLE = new EventType("IDLE");
		public static final EventType EXCEPTION = new EventType("EXCEPTION");

		private final String value;

		private EventType(String value) {
			this.value = value;
		}

		public String toString() {
			return value;
		}
	}

	protected static class Event {
		private final EventType type;

		private final NextFilter nextFilter;

		private final Object data;

		Event(EventType type, NextFilter nextFilter, Object data) {
			this.type = type;
			this.nextFilter = nextFilter;
			this.data = data;
		}

		public Object getData() {
			return data;
		}

		public NextFilter getNextFilter() {
			return nextFilter;
		}

		public EventType getType() {
			return type;
		}
	}
	
	public void sessionCreated(NextFilter nextFilter, IoSession session) {
		nextFilter.sessionCreated(session);
	}

	public void sessionOpened(NextFilter nextFilter, IoSession session) {
		fireEvent(nextFilter, session, EventType.OPENED, null);
	}

	public void sessionClosed(NextFilter nextFilter, IoSession session) {
		fireEvent(nextFilter, session, EventType.CLOSED, null);
	}

	public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) {
		fireEvent(nextFilter, session, EventType.IDLE, status);
	}

	public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) {
		fireEvent(nextFilter, session, EventType.EXCEPTION, cause);
	}

	public void messageReceived(NextFilter nextFilter, IoSession session, Object message) {
		ByteBufferUtil.acquireIfPossible(message);
		fireEvent(nextFilter, session, EventType.RECEIVED, message);
	}

	public void messageSent(NextFilter nextFilter, IoSession session, Object message) {
		ByteBufferUtil.acquireIfPossible(message);
		fireEvent(nextFilter, session, EventType.SENT, message);
	}

	protected void processEvent(NextFilter nextFilter, IoSession session, EventType type,
			Object data) {
		//----------------------------------------------------------------------
		// added by bmchae
		//----------------------------------------------------------------------
		adapter.getThreadMap().put(Thread.currentThread().getName(), Thread.currentThread());
		try {
			if (type.equals(EventType.RECEIVED)) {
				nextFilter.messageReceived(session, data);
				ByteBufferUtil.releaseIfPossible(data);
			} else if (type.equals(EventType.SENT)) {
				nextFilter.messageSent(session, data);
				ByteBufferUtil.releaseIfPossible(data);
			} else if (type.equals(EventType.EXCEPTION)) {
				nextFilter.exceptionCaught(session, (Throwable) data);
			} else if (type.equals(EventType.IDLE)) {
				nextFilter.sessionIdle(session, (IdleStatus) data);
			} else if (type.equals(EventType.OPENED)) {
				nextFilter.sessionOpened(session);
			} else if (type.equals(EventType.CLOSED)) {
				nextFilter.sessionClosed(session);
			}
		} finally {
			adapter.getThreadMap().remove(Thread.currentThread().getName());
		}
	}

	public void filterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) {
		nextFilter.filterWrite(session, writeRequest);
	}

	public void filterClose(NextFilter nextFilter, IoSession session) throws Exception {
		nextFilter.filterClose(session);
	}

	private class ProcessEventsRunnable implements Runnable {
		private final SessionBuffer sessionBuffer;
		private SocketAdapterContext context;

		ProcessEventsRunnable(SocketAdapterContext context, SessionBuffer sessionBuffer) {
			this.context = context;
			this.sessionBuffer = sessionBuffer;
		}

		public void run() {
			//------------------------------------------------------------------
			// added by bmchae
			//------------------------------------------------------------------
			if (Thread.currentThread().getName().startsWith("pool-") && context != null) {
				Thread.currentThread().setName(context.getAdapterName() + "-" + Thread.currentThread().getName());
			}

			while (!Thread.currentThread().isInterrupted()) {
				Event event;

				synchronized (sessionBuffer.eventQueue) {
					if (sessionBuffer.eventQueue.isEmpty()) {
						sessionBuffer.processingCompleted = true;
						break;
					}

					event = (Event) sessionBuffer.eventQueue.remove(0);
				}

				processEvent(event.getNextFilter(), sessionBuffer.session, event.getType(), event
						.getData());
			}

			if (logger.isDebugEnabled()) {
				logger.debug("Exiting since queue is empty for "
						+ sessionBuffer.session.getRemoteAddress());
			}
		}
	}
}
