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

import com.eactive.eai.adapter.socket2.common.ExceptionStackTrace;
import com.eactive.eai.adapter.socket2.config.SocketAdapterContext;
import com.eactive.eai.adapter.socket2.config.SocketLoggerManager;
import com.eactive.eai.common.util.Logger;
import org.apache.mina.common.IoSession;

import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;


public class SessionPool {
	private Logger logger = SocketLoggerManager.getInstance().getLogger();

	private ISocketAdapter adapter;
	private SocketAdapterContext context;
	private List<IoSession> pool;
	private List<IoSession> checkedOut;
    
    public SessionPool(ISocketAdapter adapter, SocketAdapterContext context) {
    	this.adapter = adapter;
    	this.context = context;
    	this.pool = new CopyOnWriteArrayList<>();
    	this.checkedOut = new CopyOnWriteArrayList<>();
    }
    
    public synchronized IoSession acquire(IoSession session) {
    	if (pool.remove(session)) {
    		if (!checkedOut.contains(session)) {
    			checkedOut.add(session);
    		}
    		return session;
    	}
    	
    	return null;
    }
    
    public synchronized IoSession acquire() {
		return acquire(context.getTimeout() * 1000L, true);
	}
	
    public synchronized IoSession acquireYoungest() {
		return acquire(context.getTimeout() * 1000L, false);
	}
	
	public synchronized IoSession acquire(long timeout) {
		return acquire(timeout, true);
	}

	private synchronized IoSession acquire(long timeout, boolean pickOld) {
		long startTime = System.currentTimeMillis();
        IoSession session;

        while ((session = internalAcquire(timeout, pickOld)) == null) {
            try {
            	/**
            	 * timeout의 1/10 만큼만 기다린다.
            	 */
                wait(timeout/10L);
            } catch (InterruptedException e) {
            	break;
            }

            if ((System.currentTimeMillis() - startTime) >= timeout) {
                // Timeout has expired
                break;
            }
        }
        

        if (session != null) {
        	checkedOut.add(session);
        }

        return session;
	}
	
	private IoSession internalAcquire(long timeout, boolean pickOld) {
		IoSession session = null;

        if (!pool.isEmpty()) {
            // pick the first Connection in the vector to get round-robin usage
        	if (pickOld) {
        		session = pool.remove(0);
        	} else {
        		session = pool.remove(pool.size()-1);
        	}

            try {
                // ##
                if (session == null || !session.isConnected()) {
            		if ( context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_TRACE ) {
            			logger.debug(context.getAdapterName() + " | remove bad connection from IoSessionPool.");
            		}

                    // Try again recursively
                    session = acquire(timeout, pickOld);
                }
            } catch (Exception e) {
        		if ( context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_TRACE ) {
        			logger.debug(context.getAdapterName() + " | remove bad connection from IoSessionPool.");
        		}

                // Try again recursively
                session = acquire(timeout, pickOld);
            }
        } 

        return session;
	}
	
	public synchronized void release(IoSession session) {
        if ((session != null) && session.isConnected()) {
        	if (pool.contains(session)) {
        		//throw new RuntimeException(context.getAdapterName() + " | <WARN> cannot put session into session pool. session already exists in pool (" + session + ")");
        		return;
        	}
            pool.add(session);
            checkedOut.remove(session);
            notifyAll();
        }
	}
	
	public void remove(IoSession session) {
		pool.remove(session);
		checkedOut.remove(session);
	}
	public void remove(int localPort, int remotePort){
		for (Iterator<IoSession> iii = pool.iterator() ; iii.hasNext(); ) {
	    	IoSession session = iii.next();
	    	
	    	if(session == null) return;
	    	
	    	InetSocketAddress local =(InetSocketAddress)session.getLocalAddress();
	    	InetSocketAddress remote =(InetSocketAddress)session.getRemoteAddress();
	    	
	    	if (remote != null && local.getPort() == localPort && remote.getPort() == remotePort ){
	    		pool.remove(session);
	    		try{
	    		session.close();
	    		}catch(Exception e){
	    			logger.error(e);
	    		}
	    		break;
	    	}
		}

	}
	public void sessionValidate(){
		for (Iterator<IoSession> iii = pool.iterator() ; iii.hasNext(); ) {
	    	IoSession session = iii.next();
	    	if (session!=null && !session.isConnected()){
	    		pool.remove(session);
	    		try{
	    			session.close();
	    		}catch(Exception e){
	    			logger.error(e);
	    		}
	    	}
		}
		
	}
	
	public synchronized int size() {
		return checkedOut.size() + pool.size();
	}
	
	public int peekSize() {
		return checkedOut.size() + pool.size();
	}
	
	public int checkedOut() {
		return checkedOut.size();
	}
	
	public int remaining() {
		return pool.size();
	}
	
	public Iterator<IoSession> iterator() {
		return pool.iterator();
	}

	public Iterator<IoSession> checkedOutIterator() {
		return checkedOut.iterator();
	}	

	public void shutdown() {
		int count = 0;
		while (size() > 0 && count++ < 10) {
    		if (context.getTraceLevel() >= SocketAdapterContext.TRACE_LEVEL_DEBUG) {
    			logger.debug(context.getAdapterName() + " | session pool shutdown - try count : " + count);
    		}

			for(IoSession session : checkedOut) {
				try {
					if (session != null) {
						if (session.isConnected() && !session.isClosing()) {
							session.close().join();
						}
						checkedOut.remove(session);
					}
				} catch(Exception e) {
					checkedOut.remove(session);
		        	logger.error(context.getAdapterName() + " | " + e.getMessage(), e);
					adapter.addRecentError(context.getAdapterName() + " | " + ExceptionStackTrace.dump(e));
				}
			}
	
			IoSession session = null;
			while (pool.size() > 0) {
				try {
					session = acquire(1000L, true);
					if (session != null) {
						if (session.isConnected() && !session.isClosing()) {
							session.close().join();
						}
						pool.remove(session);		
					}
				} catch(Exception e) {
					pool.remove(session);
		        	logger.error(context.getAdapterName() + " | " + e.getMessage(), e);
					adapter.addRecentError(context.getAdapterName() + " | " + ExceptionStackTrace.dump(e));
				}
			}
			
			try {
				Thread.sleep(100L);
			} catch(Exception e) {
				logger.error(context.getAdapterName() + " | session pool sleep error.", e);
			}
		}
		
		if (count >= 100) {
			logger.error(context.getAdapterName() + " | session pool shutdown failed - garbage connection left");
			adapter.addRecentError(context.getAdapterName() + " | session pool shutdown failed - garbage connection left");
		}
	}
	
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("[SessionPool of " + context.getAdapterName());
		sb.append(" | pool:" + pool.size());
		sb.append(" | checkedout:" + checkedOut.size() + "]");
		return sb.toString();
	}
}
