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

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.adapter.socket2.protocol.ISocketAdapter;
import com.eactive.eai.adapter.socket2.protocol.SessionPool;
import com.eactive.eai.common.util.Logger;
import org.apache.mina.common.IoFilterAdapter;
import org.apache.mina.common.IoSession;

import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;


public class ConnectionLimitFilter extends IoFilterAdapter {
	final protected Logger logger;
    final protected Logger errLogger;

    protected SocketAdapterContext context;
	protected ISocketAdapter adapter;

	protected ConcurrentHashMap ipLimitTable;

	public ConnectionLimitFilter(ISocketAdapter adapter) {
		this.adapter = adapter;
		this.context = adapter.getContext();
		this.ipLimitTable = new ConcurrentHashMap();

	    logger    = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());
	    errLogger = SocketLoggerManager.getInstance().getErrLogger();
	}	
	
	@Override
	public void sessionOpened(NextFilter nextFilter, IoSession session) throws Exception {
		/*
		if (context.getTraceLevel() >= context.TRACE_LEVEL_TRACE && !"HTTP".equals(context.getProtocol())) {
			logger.debug(context.getAdapterName() + " | ConnectionLimitFilter | session created : " + session);
		}
		*/
		//Session 오픈 이전에 message를 수신하지 않도록 한다.
		session.suspendRead();
		
		SessionPool sessionPool = adapter.getSessionPool();

	    /**
	     * max connection 제한 
	     */
		synchronized(this) {
		    if (context.getMaxConnection() > 0) {		    	
		    	/**
		    	 * TODO : 아래 원인을 다시 한번 체크해 볼 것
		    	 * clear invalid ('CLOSING') connections - avoid bug
		    	 */
		    	for (Iterator iter = sessionPool.iterator(); iter.hasNext(); ) {
		    		IoSession s = (IoSession) iter.next();
		    		if (s != null && s.isClosing()) {
		    			s.close();
		    			sessionPool.remove(s);
		    		}
		    	}		    	
		    	
		    	/**
		    	 * curr connections >= max.connection 이라면, 아래 2가지 중 한 작업
		    	 */
			    if (sessionPool.size() >= context.getMaxConnection()) {			    
				    /**
				     * [1] close existing 'YOUNGEST' connections
				     */
					if (context.getCloseExistingConnection()) {
						if (sessionPool.size() > 0) {
							IoSession victim = sessionPool.acquireYoungest();
							
							if (victim != null) {
								StringBuffer sb = new StringBuffer();
								sb.append(context.getAdapterName() + " | ConnectionLimitFilter | ");
								sb.append("max connection(" + context.getMaxConnection() + ") exceeded ");
								sb.append("-> remove 'YOUNGEST' victim connection in pool - [" + victim.toString() + "]");
								sb.append(", put new connection in pool - [" + session + "]");
								
								if (context.getTraceLevel() >= context.TRACE_LEVEL_INFO) {
									logger.info(sb.toString());
								}
								
								adapter.addRecentErrorOnly(sb.toString() + "\n" + dumpSessionPool());
								
								victim.close();
								sessionPool.remove(victim);
							} else {
								sessionPool.shutdown();
							}
						}
					/**
					 * [2] close new connections
					 */
					} else {
						StringBuffer sb = new StringBuffer();
						sb.append(context.getAdapterName() + " | ConnectionLimitFilter | ");
						sb.append("max connection(" + sessionPool.size() + "/" + context.getMaxConnection() + ") exceeded ");
						sb.append("-> close 'new' session - [" + session.toString() + "]");
						
						if (context.getTraceLevel() >= context.TRACE_LEVEL_INFO) {
							logger.info(sb.toString());
						}
						
						adapter.addRecentErrorOnly(sb.toString() + "\n" + dumpSessionPool());
						
						sessionPool.remove(session);
						session.close();
						
						return;
					}
			    }
			}
		}
		
	    /**
	     * remote ip 당 connection 갯수 제한 (SERVER only) 
	     */
    	synchronized (this) {
    		if (context.isServer() && context.getConnLimitPerIp() > 0) {
		    	String remoteIp = ((InetSocketAddress) session.getRemoteAddress()).getHostName();
		    	Integer remotePort = ((InetSocketAddress) session.getRemoteAddress()).getPort();
		    	
		    	if (ipLimitTable.get(remoteIp) == null) {
		    		ipLimitTable.put(remoteIp, new ConcurrentHashMap());
		    	}
		    	
		    	/**
		    	 * TODO : 아래 원인을 다시 한번 체크해 볼 것
		    	 * clear invalid ('CLOSING') connections - avoid bug
		    	 */
		    	ConcurrentHashMap map = (ConcurrentHashMap) ipLimitTable.get(remoteIp);
		    	for (Iterator iter = map.values().iterator(); iter.hasNext(); ) {
		    		IoSession s = (IoSession) iter.next();
		    		if (s != null && s.isClosing()) {
		    			s.close();
		    			map.remove(((InetSocketAddress) s.getRemoteAddress()).getPort());
		    		}
		    	}

		    	if (((ConcurrentHashMap) ipLimitTable.get(remoteIp)).size() < context.getConnLimitPerIp()) {
			    	((ConcurrentHashMap) ipLimitTable.get(remoteIp)).put(remotePort, session);
		    	} else {
		    		/**
				     * [1] close existing 'OLDEST' connections
				     * 2009.09.10 권세환대리 요청
				     * negative(-) 옵션을 준 상태에서 세션이 Full 상태 일때, 가장 오래 된 세션을 끊고 새로운 세션을 연결한다.
				     */
		    		if (context.getCloseExistingConnectionPerIpLimit()) {
		    			if (sessionPool.size() > 0) {
		    				IoSession victim = sessionPool.acquire();
		    				
		    				if (victim != null) {
								Integer victimPort = ((InetSocketAddress) victim.getRemoteAddress()).getPort();

								StringBuffer sb = new StringBuffer();
								sb.append(context.getAdapterName() + " | ConnectionLimitFilter | ");
								sb.append("connection.limit.per.ip(" + context.getConnLimitPerIp() + ") exceeded ");
								sb.append("-> remove 'OLDEST' victim connection in pool - [" + victim.toString() + "]");
								sb.append(", put new connection in pool - [" + session + "]");
								
								if (context.getTraceLevel() >= context.TRACE_LEVEL_INFO) {
									logger.info(sb.toString());
								}
								
								adapter.addRecentErrorOnly(sb.toString() + "\n" + dumpSessionPool());
								
								victim.close();
								sessionPool.remove(victim);
								
								((ConcurrentHashMap) ipLimitTable.get(remoteIp)).remove(victimPort);
								((ConcurrentHashMap) ipLimitTable.get(remoteIp)).put(remotePort, session);
							} else {
								sessionPool.shutdown();
							}
		    			}
		    		} 
					/**
					 * [2] close new connections
					 */
		    		else {
			    		StringBuffer sb = new StringBuffer();
			    		sb.append(context.getAdapterName() + " | ConnectionLimitFilter | ");
			    		sb.append("session created. : but connection.limit.per.ip(" + context.getConnLimitPerIp() + ") exceeded ");
			    		sb.append("-> close 'new' session. (" + session + ")");
			    		
						if (context.getTraceLevel() >= context.TRACE_LEVEL_INFO) {
							logger.info(sb.toString());
						}
						
						adapter.addRecentErrorOnly(sb.toString() + "\n" + 
							dumpSessionPool() + 
							dumpIpLimitTable());
						
						sessionPool.remove(session);
						session.close();
						
						return;
		    		}
		    	}
    		}
	    }
    	
    	//Connection Limit 를 체크한후 Message 를 Read 한다.
    	session.resumeRead();
		
		nextFilter.sessionOpened(session);
	}
	
	
	@Override
	public void sessionClosed(NextFilter nextFilter, IoSession session) throws Exception {
		/*
		if (context.getTraceLevel() >= context.TRACE_LEVEL_TRACE && !"HTTP".equals(context.getProtocol())) {
			logger.debug(context.getAdapterName() + " | ConnectionLimitFilter | session closed : " + session);
		}
		*/
		
	    /**
	     * remote ip 당 connection 갯수 제한 (SERVER only)
	     */
		synchronized (this) {
			if (context.isServer() && context.getConnLimitPerIp() > 0) {
		    	String remoteIp = ((InetSocketAddress) session.getRemoteAddress()).getHostName();
		    	Integer remotePort = ((InetSocketAddress) session.getRemoteAddress()).getPort();

		    	if (ipLimitTable.get(remoteIp) == null) {
		    		ipLimitTable.put(remoteIp, new ConcurrentHashMap());
		    	}
		    	
		    	((ConcurrentHashMap) ipLimitTable.get(remoteIp)).remove(remotePort);
			}
	    }	    
	
		nextFilter.sessionClosed(session);
	}
	
	
	private String dumpSessionPool() {
		StringBuffer sb = new StringBuffer();
		sb.append("SessionPool Dump : ");
		sb.append("\n<font color='gray'>");
	    for (Iterator i = adapter.getSessionPool().iterator() ; i.hasNext(); ) {
	    	IoSession session = (IoSession) i.next();
	    	sb.append("    " + sessionInfo(session) + "\n");
	    }
	    sb.append("</font>");
	    
    	sb.append("<font color='red'>");
    	for (Iterator i = adapter.getSessionPool().checkedOutIterator() ; i.hasNext(); ) {
	    	IoSession session = (IoSession) i.next();
	    	sb.append("    " + sessionInfo(session) + "\n");
	    }
	    sb.append("</font>");			    

	    return sb.toString();
	}
	
	
	private String dumpIpLimitTable() {
		StringBuffer sb = new StringBuffer();
		sb.append("IpLimitTable Dump : ");
		sb.append("\n<font color='gray'>");
	    for (Iterator<String> iter = ipLimitTable.keySet().iterator() ; iter.hasNext(); ) {
	    	String ip = iter.next();
	    	ConcurrentHashMap map = (ConcurrentHashMap) ipLimitTable.get(ip);
	    	sb.append(String.format("    * %s (%d)\n", ip, map.size()));
		    for (Iterator iter2 = map.values().iterator() ; iter2.hasNext(); ) {
		    	IoSession session = (IoSession) iter2.next();
		    	sb.append("    " + sessionInfo(session) + "\n");
		    }
	    }
	    sb.append("</font>");			    
	    return sb.toString();
	}
	
	
	private String sessionInfo(IoSession session) {
		StringBuffer sb = new StringBuffer();
    	if (!session.isConnected())
    		sb.append("●");
    	else if (session.isClosing())
    		sb.append("◐");
    	else if (System.currentTimeMillis() - session.getLastIoTime() < 1000L)
    		sb.append("⊙");
    	else
    		sb.append("○");
    	sb.append(" Session :");;
    	if (session.isClosing())
    		sb.append(" [CLOSING]");
    	if (!session.isConnected())
    		sb.append(" [NOT CONNECTED]");
    	sb.append(" " + session.getLocalAddress() + (adapter.getContext().isClient() ? "->" : "<-") + session.getRemoteAddress());
    	sb.append(", recv:" + session.getReadMessages());
    	sb.append("(" + session.getReadBytes() + ")");
    	sb.append(", send:" + session.getWrittenMessages());
    	sb.append("(" + session.getWrittenBytes() + ")");
    	sb.append(", created:" + TimeUtil.formatDate(session.getCreationTime()));
    	sb.append(", idle:" + TimeUtil.elapsedTime(session.getLastIoTime()));
		return sb.toString();
	}
}
