package com.eactive.eai.adapter.socket2.protocol;

import java.io.IOException;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.StringUtils;
import org.apache.mina.common.ConnectFuture;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.common.TransportType;
import org.apache.mina.common.WriteFuture;
import org.apache.mina.transport.socket.nio.SocketSessionConfig;

import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.EAIException;
import com.eactive.eai.adapter.RequestDispatcher;
import com.eactive.eai.adapter.socket2.EAIConnectException;
import com.eactive.eai.adapter.socket2.EAINoAvailableConnectionException;
import com.eactive.eai.adapter.socket2.EAITimeoutException;
import com.eactive.eai.adapter.socket2.SingleSessionManager;
import com.eactive.eai.adapter.socket2.common.CommonLib;
import com.eactive.eai.adapter.socket2.common.Env;
import com.eactive.eai.adapter.socket2.common.ExecutionContext;
import com.eactive.eai.adapter.socket2.common.FileUtil;
import com.eactive.eai.adapter.socket2.common.SMSLogger;
import com.eactive.eai.adapter.socket2.common.SimpleFuture;
import com.eactive.eai.adapter.socket2.common.TimeUtil;
import com.eactive.eai.adapter.socket2.config.SocketAdapterContext;
import com.eactive.eai.adapter.socket2.config.SocketLoggerManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.session.SessionManager;
import com.eactive.eai.common.session.SessionVO;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.inbound.processor.GWSessionManager;
import com.eactive.eai.util.HealthCheckManager;

public abstract class ProtocolHandler extends IoHandlerAdapter {
	protected final Logger logger;
	protected final Logger errLogger;

	private final Object lockObj = new Object();
	private static final String HANDLER_STATE_KEY = ProtocolHandler.class.getName() + ".STATE";
	public static final String SESSION_STATE = "SESSION_STATE";
	public static final String CODEC_STATE = "CODEC_STATE";
	public static final String CODEC_STATE_BUFFER = "CODEC_STATE_BUFFER";
	public static final String HANDSHAKE_FUTURE = "HANDSHAKE_FUTURE";
	
	private enum State {
		STATE_MESSAGE_WAIT_REQUEST, 
		STATE_MESSAGE_WAIT_RESPONSE;
	}

	private static class HandlerState {
		State state = State.STATE_MESSAGE_WAIT_REQUEST;
		SimpleFuture responseFuture = new SimpleFuture();
	}
	
	protected SocketAdapterContext context;
	protected ISocketAdapter adapter;
	protected ArrayList<Thread> retryThreads;
	protected SessionPool sessionPool;
	
	protected long messageSend = 0;
	protected long messageRecv = 0;
	private boolean isXCInclude = false;//XecuserConnect 모듈 유무 판단.	
	
	protected ProtocolHandler(ISocketAdapter adapter) {
		this.adapter = adapter;
		this.context = adapter.getContext();

	    logger    = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());
	    errLogger = SocketLoggerManager.getInstance().getErrLogger();

		this.sessionPool = new SessionPool(adapter, context);
		//XecuserConnect 모듈 유무 판단.
		try {
			Class clazz = Class.forName("com.sf.xc3.XCException");
			isXCInclude = true;
			logger.warn("XCException include");
		} catch (Throwable e) {
			logger.error("XCException not include");
		}
	}

	public void start() throws Exception {
		this.retryThreads = new ArrayList<>();	
	}
	
	public void stop() throws Exception {
		for (Iterator<Thread> iter = (new ArrayList<Thread>(retryThreads)).iterator(); iter.hasNext(); ) {
			try {
				iter.next().interrupt();
			} catch(ConcurrentModificationException e) {
				logger.debug("stop ConcurrentModificationException");
			}
		}
	}
	
	@Override
	public void sessionCreated(IoSession session) throws Exception {
		super.sessionCreated(session);
		
		synchronized(lockObj) {
			HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
			if (handlerState == null) {
				handlerState = new HandlerState();
				session.setAttribute(HANDLER_STATE_KEY, handlerState);
				setSessionState(session, handlerState.state.toString());		
				
				SimpleFuture handshakeFuture = new SimpleFuture();
				session.setAttribute(HANDSHAKE_FUTURE, handshakeFuture);
			}
		}
	}
	
	@Override
	public void sessionOpened(IoSession session) throws Exception {
		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_TRACE && !"HTTP".equals(context.getProtocol())) {
			logger.debug(context.getAdapterName() + " | session opened : " + session);
		}			
		
		// DEFAULT : false
		if(session.getTransportType() == TransportType.SOCKET
				&& context.isKeepAlive()) {
			((SocketSessionConfig)session.getConfig()).setKeepAlive(true);
		}
		
		if (context.isPollingUse() && context.isPollingSender() && context.isPermanent()) {
			session.setIdleTime(IdleStatus.BOTH_IDLE, context.getPollingTimeout());
		} else {
			session.setIdleTime(IdleStatus.BOTH_IDLE, context.getIdleTimeout());
		}
		session.setWriteTimeout(context.getTimeout());
		
		HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
		if (handlerState == null) {
			handlerState = new HandlerState();
			handlerState.state = State.STATE_MESSAGE_WAIT_REQUEST;
			session.setAttribute(HANDLER_STATE_KEY, handlerState);
		}
		
		synchronized(lockObj) {
			sessionPool.release(session);
			
			if (!context.isPermanent()) {
				SimpleFuture handshakeFuture = (SimpleFuture) session.getAttribute(HANDSHAKE_FUTURE);
				if (handshakeFuture != null) {
					handshakeFuture.set(Boolean.TRUE);
				}
			}
			
			adapter.setAdapterStatus(true);
		}
		if("Y".equals(context.getSingleSession())){
			logger.debug(context.getAdapterName() + " | session opened : SingleSession - " + session);
			SingleSessionManager.getInstance().sessionOpen(context.getAdapterGroupName(), session);
		}
	}
	
	@Override
	public void sessionIdle(IoSession session, IdleStatus status) {
		HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);

		StringBuilder sb = new StringBuilder();
		sb.append("idle:" + TimeUtil.elapsedTime(session.getLastIoTime()));
		if (System.currentTimeMillis()-session.getLastIoTime() < (context.getTimeout() + 1) * 1000L) {
			sb.append(", timeout:" + context.getTimeout());
			sb.append(", protocol:" + context.getProtocol());
			if (handlerState != null) {
				sb.append(", state:" + handlerState.state.toString());
			}
			sb.append("/" + session.getAttribute(SESSION_STATE));
			sb.append("/" + session.getAttribute(CODEC_STATE));	
			sb.append(", " + session.getAttribute(CODEC_STATE_BUFFER));
			
			logger.error(context.getAdapterName() + " | transaction timeout -> disconnecting (" + session + ") " + sb.toString());
			adapter.addRecentError(context.getAdapterName() + " | transaction timeout -> disconnecting (" + session + ") " + sb.toString());
//			SMSLogger.loggingSMSWithCode(context.getAdapterName(), "RECEAINNA005");
			SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA005");
		} else {
			/**
			 * idle.timeout이 0 이면 connection을 끊지 않는다.
			 */
			if (context.getIdleTimeout() == 0) {
				return;				
			}
			
    		adapter.addRecentErrorOnly(context.getAdapterName() + " | session idle -> disconnecting (" + session + ") " + sb.toString());
			logger.info(context.getAdapterName() + " | session idle -> disconnecting (" + session + ") " + sb.toString());
		}
		
		// disconnect an idle client
		sessionPool.remove(session);
		session.close();
	}

	@Override
	public void sessionClosed(IoSession session) throws Exception {
		try {		
			/**
			 * logging
			 */
			if (context.isPermanent() && context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) {
				/**
				 * ignore L4 connection close event
				 */
				if (session.getWrittenMessages()==0 && session.getReadMessages()==0 
						&& System.currentTimeMillis() - session.getCreationTime() < 1000) {
					// ignore - skip logging
				} else {
					adapter.addRecentError(context.getAdapterName() + " | session closed (" + session + ")");
				}
			}
		
			if (context.isPermanent() && context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_TRACE && !"HTTP".equals(context.getProtocol())) {
				logger.debug(context.getAdapterName() + " | session closed (" + session + ")" + "");
			}		
	
			HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
			if (handlerState != null 
				 && handlerState.responseFuture != null
				 && (context.isServer() || context.isPermanent())) {
				handlerState.responseFuture.set(new Exception("session closed by peer (" + session + ")"));
			}
			
			ConnectFuture connectFuture = (ConnectFuture) session.getAttribute("connectFuture");
			if (connectFuture != null) {
				connectFuture.setException(new Exception("inbound session is closed by peer (" + session + ")"));
			}
	
			if (!context.isPermanent()) {
				return;
			}
			
			if (adapter.getState() == ISocketAdapter.State.STOPPING || adapter.getState() == ISocketAdapter.State.STOPPED) {
				return;
			}
		} finally {
			try {
				if("Y".equals(context.getSingleSession())){
					SingleSessionManager.getInstance().sessionClose(context.getAdapterGroupName(), session);
				}
				
				sessionPool.remove(session);
				session.close();

				if (sessionPool.size() <= 0 && context.isPermanent()) {
					adapter.setAdapterStatus(false);
				}
			} catch(Exception e) {
				logger.error(context.getAdapterName() + " | " + e.getMessage(), e);
			}
		}

		if (adapter instanceof SocketAdapterClient 
			&& context.isPermanent() 
			&& adapter.getState() == ISocketAdapter.State.STARTED
			&& adapter.getSessionPool().size() < adapter.getContext().getMaxConnection()
			&& !("INISAFE".equals(context.getProtocol())
					|| "INISAFE_2020".equals(context.getProtocol())
					|| "INISAFE_NATN".equals(context.getProtocol())
					)) {
			
			if ( context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {				
				logger.debug(context.getAdapterName() + " | session closed ->  try to connect after 10 seconds ...");
			}		

			while (!Thread.currentThread().isInterrupted()) {
				if (adapter.getState() != ISocketAdapter.State.STARTED) {
					break;
				}
				
				// 싱글어댑터 - Start ---------------------------------------------------------------------------------------------------------------------
				boolean retry = true;
				
				// 싱글어댑터 session 종료 시 재연결 처리
				if (context.getSingleSession().equals("Y")) {
					String primaryInst = context.getPrimaryInstId();
					EAIServerManager eaiServerManager = EAIServerManager.getInstance();
					String localServerName = eaiServerManager.getLocalServerName();
					String adapterGroupName = context.getAdapterGroupName();
					
					if (localServerName.equals(primaryInst)) { // CASE : Primary 서버
						SessionVO sessionVO = SessionManager.getInstance().getConnectionStatus(adapterGroupName);
						
						if(sessionVO != null) { // Secondary 서버가 연결하고 있음
							retry = false;
							if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
								logger.debug(context.getAdapterName() + "| SingleSession Validation Exist [" +sessionVO.getLoginInstId()+"]");
							}
							
						} else { // Secondary 서버가 연결하지 않고 있으므로 연결 시도함.
							if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
								logger.debug(context.getAdapterName() + "| SingleSession Validation Connection empty -> retry to connect after 10 secs");
							}							
						}
					} else { // CASE : Secondary 서버
						boolean isAlive = HealthCheckManager.getInstance().getServerStatus(primaryInst); // Primay서버 상태 확인
						
						if (isAlive) { // Primary 서버 살아있으면 재연결 시도 안함
							retry = false;
							if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG)
								logger.debug(context.getAdapterName() + "| SingleSession Validation Exist [" +primaryInst+"]");
						} else {
							SessionManager.getInstance().removeConnectionInfo(adapterGroupName);
						}
					}
					
				} 
				
				if(!retry) {
					try {
						Thread.sleep(10 * 1000L);
					} catch (InterruptedException e) {						
						logger.error(context.getAdapterName() + " | " + e.getMessage(), e);
					} 
					continue;
				}
				// 싱글어댑터 - End ---------------------------------------------------------------------------------------------------------------------
				/**
				 * FDS - connectOne 시도 후, 바로 connection이 끊어지는 경우
				 * ExecutorFilter에서 Thread 생성이 계속 늘어나는 현상을 방지
				 * connection retry thread 개수를 max.connection 개수에 맞춘다.
				 */			
				synchronized(retryThreads) {
					if (sessionPool.size() + retryThreads.size() >= context.getMaxConnection()) {
						break;
					}					
					retryThreads.add(Thread.currentThread());
				}
			
				try {
					try { 
						Thread.sleep(10000L);
					} catch(InterruptedException ee) {
						break;
					} catch(Exception ee) {
			        	logger.error(context.getAdapterName() + " | " + ee.getMessage(), ee);
					}
					
//					if (adapter.getState() != ISocketAdapter.State.STARTED) {
//						break;
//					}

					try {
						if (adapter.connectOne() != null) {
							break;
						}
					} catch(Throwable t) {
						logger.debug(context.getAdapterName() + " | session closed -> fail to reconnect -> retry after 10 seconds. caused by " + t.toString());
					}
				} finally {
					synchronized(retryThreads) {
						retryThreads.remove(Thread.currentThread());
					}
				}
			}
		}
	}

	@Override
	public void exceptionCaught(IoSession session, Throwable cause) {
		if (cause instanceof IOException) {
			logger.warn(context.getAdapterName() + " | " + cause.getMessage() + " (" + session + ")");
			String cMsg = cause.getMessage();
			if( (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_INFO) && context.isPermanent() 
				&& (cMsg != null && cMsg.indexOf("reset by peer") != -1) ) {
				adapter.addRecentError(context.getAdapterName() + " | " + cMsg + " (" + session + ")", cause);
			}
				
		} else if (isXCInclude && cause instanceof com.sf.xc3.XCException) {
			logger.warn(context.getAdapterName() + " | session exception caught (" + session + ") - " + cause.getMessage());
		} else {
			String errorMsg = context.getAdapterName() + " | session exception caught (" + session + ")";
			logger.error(errorMsg, cause);
			adapter.addRecentError(errorMsg, cause);
			errLogger.error(errorMsg, cause);
			SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA001");
		}
		
		HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
		if (handlerState != null && handlerState.responseFuture != null
			 && cause instanceof IOException
			 && (context.isServer() || context.isPermanent())) {
			handlerState.responseFuture.set(cause);
		}
		
		// close the connection on exceptional situation
		// TODO : 위의 sessionClosed() 메쏘드가 불려져서 pool에서 제거되는지 확인
		sessionPool.remove(session);
		session.close();
	}

	public SocketAdapterContext getContext() {
		return context;
	}	
	
	public SessionPool getSessionPool() {
		return sessionPool;
	}
	
	/**
	 * message received
	 */
	@Override
	public void messageReceived(final IoSession session, Object message) throws Exception {
		if ( context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG  && !"HTTP".equals(context.getProtocol())) {
			logger.debug(context.getAdapterName() + " (" + session + ") | <RECV> " + message);
		} 

		HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
		if (handlerState == null) {
			handlerState = new HandlerState();
			handlerState.state = State.STATE_MESSAGE_WAIT_REQUEST;
			session.setAttribute(HANDLER_STATE_KEY, handlerState);
		}
		
		incMessageRecv();

		ByteMessage m = (ByteMessage) message;

		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
		    adapter.addRecentRecv(session, CommonLib.hexDump(m.getBytes(), true, context));
		}
		
		switch (handlerState.state) {
		case STATE_MESSAGE_WAIT_RESPONSE:		    
			if (handlerState.responseFuture != null) {
				handlerState.responseFuture.set(m);
			}
			if (context.isPermanent() && (!session.isConnected() || session.isClosing())) {
				throw new IOException("session is not connected (" + session + ")");
			}
			break;
			
		case STATE_MESSAGE_WAIT_REQUEST:
			try {
				sessionPool.acquire(session);
				
				byte[] r = null;
				if (message instanceof ByteMessage) {
					r = ((ByteMessage) message).getBytes();			
				} else {
					throw new Exception("unknown message type : " + message.getClass().getName());
				}

				if ("OUTBOUND".equals(context.getBoundUsage())) {
					throw new Exception("'OUTBOUND" + (context.isSync() ? "" : ", ASYN") + "' adapter cannot process request : " + CommonLib.hexDump(r, true, context));
				}
				
				final byte[] request = r;
				@SuppressWarnings("unchecked")
				final FutureTask futureTask = new FutureTask(new Callable() {
					ExecutionContext executionContext = ExecutionContext.getInstance();
					
					public byte[] call() throws Exception {
						try {
							if (Thread.currentThread().getName().startsWith("pool-")) {
								Thread.currentThread().setName(context.getAdapterName() + "-Worker-" + Thread.currentThread().getName());
							}
							executionContext.set("session", session);
							return processRequest(session, request);
						} finally {
							executionContext.remove("session");
						}
					}
				});
				
				try  {		
					futureTask.run();
				} catch(Exception e) {
					throw new RuntimeException("error when process request", e);
				}
				
				if (context.isPermanent() && (!session.isConnected() || session.isClosing())) {
					throw new IOException("session is not connected (" + session + ")");
				}
				
				/**
				 * if SYNC -> send result to sender
				 */			
				if (context.isSync()) {
					try {
						byte[] response = (byte[]) futureTask.get(context.getTimeout(), TimeUnit.SECONDS);
						
						if (response == null) {
							throw new EAIException("cannot send NULL response message (framework returned) - check if target adapter is ASYNC");
						} else if (response.length == 0) {
							throw new EAIException("cannot send ZERO-size response message (framework returned) - check if target adapter is ASYNC");
						} else {
							try {
								internalSendMessage(session, new ByteMessage(response));
							} catch(Exception e) {
								throw new EAIException("(SYNC) cannot send response message", e);
							}
						}
					} catch (ExecutionException e) {
						Throwable cause = e.getCause();
						if (cause instanceof Exception)
							throw (Exception) cause;
						else
							throw new Exception(cause.getMessage());
					} catch (InterruptedException e) {
						throw new TimeoutException("(future) cannot recv message. timeout=" + context.getTimeout());
					} catch(TimeoutException e) {
						throw new TimeoutException("(future) cannot send message. timeout=" + context.getTimeout());
					}
				}
			} finally {
				try {
					releaseSession(session);
				} catch(Exception e) {
					logger.warn("releaseSession error", e);
				}
			}
			
			break;
		}
	}
	
	protected byte[] processRequest(IoSession session, byte[] request) throws Exception {
		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_TRACE) {
			logger.debug(context.getAdapterName() + " | (before) process request : " + CommonLib.hexDump(request, true, context));
		}

        try {   
			Properties prop = new Properties();
			byte[] response = null;
			
			//for GW
			AdapterManager adapterManager = AdapterManager.getInstance();
			AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(context.getAdapterGroupName());
			
//			if (context.getBoundUsage().equals(context.IOBOUND_SOCKET)) {
			if (StringUtils.isNotBlank(adptGrpVO.getSameSessionYn()) && "1".equals(adptGrpVO.getSameSessionYn())
					&& context.getBoundUsage().equals(context.IOBOUND_SOCKET)) {
				String sessionId = String.valueOf(session.hashCode());
				prop.setProperty("SESSION_ID", sessionId);
				GWSessionManager.getInstance().sessionIn(sessionId, session);
			}

			if (context.getRequestProcessor() != null) {
				response = context.getRequestProcessor().handle(request);
			} else if (!"".equals(context.getScript().trim())) {
				String filename = context.getScript();
				if (filename.endsWith(".groovy")) {
					Groovy groovy = new Groovy(filename,
							context.getAdapterGroupName() + "." + context.getAdapterName());
					response = groovy.run(request);
				} else {
					response = FileUtil.readFile(filename);
				}
			} else if (Env.isEcho() || context.getDescription().startsWith("ECHO")) {
				response = request;
			} else if (!Env.isStandalone()) {
				
				String encode = null;
				try {
					RequestDispatcher dispatcher = RequestDispatcher
							.getRequestDispatcher(context.getAdapterGroupName());
					Object responseObject = dispatcher.handle(context.getAdapterName(), request, prop);
					if (responseObject == null) {
						response = null;
					} else if (responseObject instanceof byte[]) {
						response = (byte[]) responseObject;
					} else if (responseObject instanceof String) {
						// TODO : 정상작동여부를 확인해야 함. 
						encode = prop.getProperty(com.eactive.eai.adapter.Keys.CHARSET_KEY_IN, "euc-kr");
						response = ((String) responseObject).getBytes(encode);
					} else {
						throw new Exception("RECEAIAHA001");
					}

					if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
						logger.debug(context.getAdapterName() + " | (after) process request : "
								+ CommonLib.hexDump(response, true, context));
					}

					return response;
				} catch (Throwable t) {
					logger.error(t);
					byte[] errorResponse = ("[RECEAIONA001] " + context.getAdapterName()
							+ " | cannot process request - " + t.getMessage()).getBytes(encode);
					internalSendMessage(session, new ByteMessage(errorResponse));
					throw t;
				}
			} else {
				throw new Exception("no rule to handle request");
			}

			return response;
        } catch (Throwable t) {
        	logger.error(context.getAdapterName() + " | cannot process request " + CommonLib.hexDump(request, true, context), t);
    		adapter.addRecentError(context.getAdapterName() + " | cannot process request " + CommonLib.hexDump(request, true, context), t);
    		SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA010");

    		HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);
    		if (handlerState != null && handlerState.responseFuture != null) {
    			handlerState.responseFuture.set(t);
    		}
    		throw new EAIException(t);
        }
	}

	protected void internalSendMessage(IoSession session, Object message) throws Exception {	
		if(!session.isConnected()) {
			throw new EAIException("cannot send message - session is not connected (" + session + ")");
		}

		if ( context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG ) {
			logger.debug(context.getAdapterName() + " | <SEND> " + getHexDump(message));
		    adapter.addRecentSend(session, getHexDump(message));
		}

		WriteFuture future = session.write(message);
		if (adapter instanceof TcpAdapterClient || adapter instanceof TcpAdapterServer) {
			future.join(context.getTimeout() * 1000L);
		}
		
		incMessageSend();
	}		
	
	public Object sendMessage(IoSession reservedSession, Object message) throws EAIException {
		return sendMessage(reservedSession, message, context.getTimeout());
	}
	public Object sendMessage(IoSession reservedSession, Object message, int responseTimeout) throws EAIException {
		switch(adapter.getState()) {
		case STARTING:
 		case STARTED:
			break;
		default:
    		adapter.addRecentError(context.getAdapterName() + " | cannot send message. adapter state is " + adapter.getState().name());
			throw new EAIException(context.getAdapterName() + " | cannot send message. adapter state is " + adapter.getState().name());
		}
		
		IoSession session = null;			

		try {
			if (reservedSession == null) {
				session = acquireSession();
			} else {
				session = reservedSession;
			}

			HandlerState handlerState = (HandlerState) session.getAttribute(HANDLER_STATE_KEY);		
			handlerState.responseFuture = new SimpleFuture();
			if (context.isSync()) {
				handlerState.state = State.STATE_MESSAGE_WAIT_RESPONSE;
			}			

			internalSendMessage(session, message);

			if(!session.isConnected() && (context.isServer() || context.isPermanent())) {
				String logMessage = context.getDescription() + "(" + context.getAdapterName() + ") | (server||permanent) SEND 후, 세션 연결 끊김 (" + session + ")";
				logger.error(logMessage);
				throw new EAIException(logMessage);
			}

			if (context.isSync()) {
				try {
					session.setAttribute("responseTimeout",responseTimeout);
					session.setIdleTime(IdleStatus.BOTH_IDLE, responseTimeout);
					
					Object result = handlerState.responseFuture.join(responseTimeout * 1000L);
					session.removeAttribute("responseTimeout");
					
					if (result instanceof TimeoutException) {
						throw new EAITimeoutException("timeout(" + responseTimeout + "), cannot receive result message");
					} else if (result instanceof Throwable) {
						throw new EAIException("send/recv(SYNC) faild - " + ((Exception) result).getMessage(), (Throwable) result);
					} else {
						ByteMessage m = (ByteMessage) result;
						if (m.size() >= 7 && new String(m.getBytes(0,7)).startsWith("[RECEAI"))
							throw new EAIException(m.getString());
						return m.getBytes();
					}
				} finally {
					handlerState.state = State.STATE_MESSAGE_WAIT_REQUEST;
				}	   
			} else {	
				return null;
			}
		} catch(EAITimeoutException e) {
			sessionPool.remove(session);
			if(session != null) session.close();    		
			String errorMessage = context.getAdapterName() + " | send(/recv) EAITimeoutException - " + e.getMessage();
    		adapter.addRecentError(errorMessage, e);
			throw new EAITimeoutException(errorMessage, e);
		} catch(EAIException e) {
			sessionPool.remove(session);
			if(session != null) session.close();
			String errorMessage = context.getAdapterName() + " | send(/recv) EAIException - " + e.getMessage();
    		adapter.addRecentError(errorMessage, e);
			throw new EAIException(errorMessage, e);
		} catch(TimeoutException e) {
			sessionPool.remove(session);
			if(session != null) session.close();
			String errorMessage = context.getAdapterName() + " | send(/recv) TimeoutException - " + e.getMessage();
    		adapter.addRecentError(errorMessage, e);
			throw new EAITimeoutException(errorMessage, e);
		} catch(Throwable e) {
			sessionPool.remove(session);
			if(session != null) session.close();
			String errorMessage = context.getAdapterName() + " | send(/recv) Throwable - " + e.getMessage();
    		adapter.addRecentError(errorMessage, e);
			throw new EAIException(errorMessage, e);
		} finally {			
			if (reservedSession == null) {
				releaseSession(session);
			}
		}
	}
		

	public IoSession acquireSession() throws EAIException {
		IoSession session = null;
		
		if (context.isClient() && !context.isPermanent()) {
			session = adapter.connectOne();
			
			if (session == null) {
				String errorMsg = context.getAdapterName() 
						+ " | cannot connect -> " + context.getHostName() + "/" + context.getPortNumber();
				logger.error(errorMsg);
				adapter.addRecentError(errorMsg);
				if (TimeUtil.isSMSRunningTime(context.getSmsRunningTime())) 
					SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA014");
				throw new EAIConnectException(errorMsg);
			}
		
			SimpleFuture handshakeFuture = (SimpleFuture) session.getAttribute(HANDSHAKE_FUTURE);
			if (handshakeFuture != null) {
				try {
					Object result = handshakeFuture.join(context.getTimeout() * 1000L);
					if (result instanceof Exception) {
						throw new EAIConnectException(context.getAdapterName() + " | cannot connect error", (Exception) result);
					}
				} catch(EAITimeoutException e) {
					throw new EAIConnectException(context.getAdapterName() + " | handshake faild due to timeout", e);
				}
			}
		} else {
			/**
			 * client && pool size = 0 일 경우, 10ms만 기다린다.
			 */
			long timeout = context.getTimeout() * 1000L;
			if (adapter.getState()==ISocketAdapter.State.STARTED && sessionPool.size()<1) {
				timeout = 10L;
			}
			
			session = sessionPool.acquire(timeout);
			
			if (session == null) {
				SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA015");
				throw new EAINoAvailableConnectionException("cannot get session(=connection) from SessionPool[size=" + sessionPool.size() + ", remaining=" + sessionPool.remaining() + "]");
			}
		}
		
		return session;
	}
	
	public void releaseSession(IoSession session) {
		if (session == null) {
			return;
		}
		
		if (context.isClient() && !context.isPermanent()) {
			sessionPool.remove(session);
			session.close();
		} else {
			if (session.isClosing()) {
				sessionPool.remove(session);
			} else {
				sessionPool.release(session);
			}
		}
	}
	
	public Object sendBatch(Object message) throws Exception {
		return null;
		/*
	    if (context.getSocketListener() != null) {
	    	return ((SocketListener) context.getSocketListener()).messageReceived(request);
	    }
	    */
	}
	
	public long getMessageSend() {
		return messageSend;
	}
	
	public long getMessageRecv() {
		return messageRecv;
	}
	
	public synchronized void incMessageSend() {
		messageSend++;
	}
	
	public synchronized void incMessageRecv() {
		messageRecv++;
	}
	
	public void clearStats() {
		messageSend = 0;
		messageRecv = 0;
	}

	protected String getSessionState(IoSession session) {
		return (String) session.getAttribute(SESSION_STATE);
	}
	
	protected void setSessionState(IoSession session, String state) {
		session.setAttribute(SESSION_STATE, state);
	}
	
	protected String getHexDump(Object message) {
		if (message == null) {
			return "";
		} else if (message instanceof byte[]) {
			message = new ByteMessage((byte[]) message);
		    return ((ByteMessage) message).hexDump();
		} else if (message instanceof String) {
			message = new ByteMessage((String) message);
			return ((ByteMessage) message).hexDump();
		} else {
		    return message.toString();
		}		
	}
}
