package com.eactive.eai.adapter.socket.service;

import com.eactive.eai.adapter.socket.common.CommonLib;
import com.eactive.eai.adapter.socket.common.DiagLogger;
import com.eactive.eai.adapter.socket.common.SocketAdapterException;
import com.eactive.eai.adapter.socket.config.ConfigurationContext;
import com.eactive.eai.adapter.socket.config.SocketAdapterManager;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Vector;

import java.nio.channels.ClosedSelectorException;
import java.util.HashMap;


public class SocketServer extends Thread implements SocketService {

    private ConfigurationContext    context;
    private Vector<SocketService>   threadPool;
    private boolean                 active;
    private Selector                selector;
    private ServerSocketChannel     serverSocketChannel;
    private ServerSocket            serverSocket;
    private Logger                  logger;
    private HashMap<String, Integer> ipTable;		// IP별 Connection 수 제한이 있는 경우 , KEY-RemoteIP, VALUE-Integer

    //소켓 관련 프라퍼티 정보
	public static final String PROP_GROUP_SOCKET     = "socket";
	public static final String PROP_BLOCK_IP_LIST    = "block.ip.list";  //연결 거부 IP 리스트
	public static final String PROP_BLOCK_SLEEP      = "sleep.time";     //close 하기 전에 대기 시간

    public SocketServer(ConfigurationContext context) {
        String name = this.getName();
        this.setName( context.getAdapterName() + "-LISTENER" + name.substring( name.lastIndexOf("-")) );

        this.context    = context;
        active          = true;
        threadPool      = new Vector<SocketService>();
        logger          = (Logger) SocketLogManager.getInstance().getLogger( this.context.getAdapterGroupName() );
        ipTable         = new HashMap<String, Integer>();
    }

    private int getTraceLevel() {
    	return context.getTraceLevel();
    }

    public void run() {
        // Server Socket을 기동하는 순간 Connection없음을 나타내도록 Adapter Status를 false로 변경한다.
        SocketAdapterManager.getInstance().changeAdapterStatus( this, false);
        try {
            openSelector();
        } catch(IOException e) {
            logger.error(CommonLib.getMessage("BECEAIMSA029", new String[] { context.getAdapterName(), context.getHostName() + ":" + context.getPortNumber(), e.getMessage() } ), e);
            active = false;
            SocketAdapterManager.getInstance().stop( context.getAdapterGroupName(), context.getAdapterName() );
        }

        Iterator<SelectionKey>		it;
        SocketChannel   channel;
        SelectionKey    key;
        while ( active ) {
            try {
                // select(miliseconds) : 대기시간
                int i = selector.select(2000L);
                if(i == 0) {
                    if ( threadPool == null || threadPool.size() == 0 ) {
                        SocketAdapterManager.getInstance().changeAdapterStatus( this, false );
                    }
                    continue;
                }
            } catch(IOException e) {
                active = false;
                shutdown();
                break;
            } catch( ClosedSelectorException e ) {
            } catch( Exception e ) {
            	continue;
            }

            if ( selector == null || !selector.isOpen() ) break;

            it = selector.selectedKeys().iterator();

            while( it.hasNext() ) {
                // 소켓 서버로 연결 요청이 있는 경우.
                try {
                    key  = it.next();
                    it.remove();
                    if ( !key.isValid() ) continue;
                    ServerSocketChannel server  = (ServerSocketChannel)key.channel();
                    if ( !server.isOpen() ){
                    	continue;
                    }
                    
                    channel = server.accept();

                    if(channel == null) continue;

                    // L4에서 보내온 세션인 경우 
                    String remoteIp = channel.socket().getRemoteSocketAddress().toString().substring(1,channel.socket().getRemoteSocketAddress().toString().indexOf(":") );
                    String blockIpList = PropManager.getInstance().getProperty(PROP_GROUP_SOCKET, PROP_BLOCK_IP_LIST);

                    if (( blockIpList != null) && ( blockIpList.indexOf(remoteIp) > -1 ) ) {

                        try {
                        	long delayTime = 1000;
                        	try {
                        		delayTime = Long.parseLong(PropManager.getInstance().getProperty(PROP_GROUP_SOCKET, PROP_BLOCK_SLEEP));
							} catch (Exception e) {}
							if ( delayTime > 0 )
								Thread.sleep(delayTime);
                            channel.socket().close();
                            channel.close();
                        } catch(IOException ie) {}
                        continue;
                    }
                    
                    if ( context.getMaxConnection() > 0 && context.getMaxConnection() <= threadPool.size() ) {
                    	try {
                        	for ( int inx = 0; inx < threadPool.size(); inx++){
                            	SocketService oldService = threadPool.get(inx);
                            	if ( oldService.isActive()){
                                    logger.warn( CommonLib.getMessage("BECEAIMSA022", new String[]{ context.getAdapterName(), Integer.toString( context.getMaxConnection() ),this.getSocketInfo( oldService.getCurrentSocket() ) } ) );
                                	oldService.shutdown();
                            	}
                        	}
						} catch (Exception e) {
							logger.error( e.getMessage(), e);
						}
                    }
                    // Client Socket별 (REMOTE IP) Connection 수 제한 있는 경우
                    if ( context.getConnLimitPerIp() > 0 ) {
                        String 	ip		 	= channel.socket().getInetAddress().getHostAddress();
                        Integer	connCount 	= ipTable.get( ip );
                        if ( connCount == null ) {
                            ipTable.put( ip, new Integer( 1 ) );
                        } else if ( connCount.intValue() < context.getConnLimitPerIp() ) {
                            int newCount = connCount.intValue() + 1;
                            ipTable.put( ip, new Integer( newCount ));
                        } else {
                            logger.error( CommonLib.getMessage("BECEAIMSA023", new String[]{ context.getAdapterName(), Integer.toString( context.getConnLimitPerIp() ),this.getSocketInfo( channel ) } ) );
                            try {
                                channel.socket().close();
                                channel.close();
                            } catch(IOException ie) {}
                            continue;
                        }
                    }

                    if ( getTraceLevel() >= DiagLogger.INFO ) {
                    	logger.info( CommonLib.getMessage("BICEAIMSA033", new String[]{ context.getAdapterName(), this.getSocketInfo( channel ) } ) );
                    }

                    if ( this.context.isSocketReuse() ) {
                        channel.socket().setSoLinger(true, 100);
                    }

                    // 신규 Connection Process 쓰레드를 생성 후 수행시킨다...
                    if ( isForInbound() && context.getSocketType().equals( ConfigurationContext.SERVER_SOCKET ) ) {
                        InboundServer aProcess = new InboundServer(context, channel, this);
                        logger.debug("# INBOUND SOCKETSERVER CREATED.[" + this.getName() + "]");
                        //
                        threadPool.addElement( aProcess );
                        aProcess.start();
                        logger.debug("# INBOUND SOCKETSERVER STARTED.[" + this.getName() + "]");
                        synchronized ( threadPool ) {
                            if ( threadPool.size() == 1 ) {
                                SocketAdapterManager.getInstance().changeAdapterStatus( this, true );
                            }
                        }
                    } else if ( isForOutbound()  )  {
//                        OutboundServer aProcess = new OutboundServer(context, channel, this);
//                        logger.debug("# OUTBOUND SOCKETSERVER CREATED.[" + this.getName() + "]");
//                        if ( !OutboundChannelManager.getInstance().hasChannel( context.getAdapterGroupName() )) {
//                                        OutboundChannel connection = new OutboundChannel(context.getSessionTimeout()*1000);
//                                        OutboundChannelManager.getInstance().addConnectionGroup( context.getAdapterGroupName(), connection );
//                        }
//                        threadPool.addElement( aProcess );
//                        aProcess.start();
//                        logger.debug("# OUTBOUND SOCKETSERVER STARTED.[" + this.getName() + "]");
//                        synchronized ( threadPool ) {
//                            if ( threadPool.size() == 1 ) {
//                                SocketAdapterManager.getInstance().changeAdapterStatus( this, true );
//                            }
//                        }
                    } else if ( isForIobound()  )  {
                    	IOboundServer aProcess = new IOboundServer(context, channel, this);
                        logger.debug("# IOBOUND SOCKETSERVER CREATED.[" + this.getName() + "]");
                        if ( !OutboundChannelManager.getInstance().hasChannel( context.getAdapterGroupName() )) {
                            OutboundChannel connection = new OutboundChannel(context.getSessionTimeout()*1000);
                            OutboundChannelManager.getInstance().addConnectionGroup( context.getAdapterGroupName(), connection );
                        }
                        threadPool.addElement( aProcess );
                        aProcess.start();
                        logger.debug("# IOBOUND SOCKETSERVER STARTED.[" + this.getName() + "]");
                        synchronized ( threadPool ) {
                            if ( threadPool.size() == 1 ) {
                                SocketAdapterManager.getInstance().changeAdapterStatus( this, true );
                            }
                        }
//                    } else {
//                        AdminServer aProcess = new AdminServer(context, channel, this);
//                        logger.debug("# ADMIN SOCKETSERVER CREATED.[" + this.getName() + "]");
//                        threadPool.addElement( aProcess );
//                        aProcess.start();
//                        logger.debug("# ADMIN SOCKETSERVER STARTED.[" + this.getName() + "]");
//                        synchronized ( threadPool ) {
//                            if ( threadPool.size() == 1 ) {
//                                SocketAdapterManager.getInstance().changeAdapterStatus( this, true );
//                            }
//                        }
                    }
                    // Process 쓰레드 Started
                } catch(Exception e) {
                    logger.error(CommonLib.getMessage("BECEAIMSA030", new String[] { context.getAdapterName(), e.getMessage() } ), e);
                    logger.error( e.getMessage(), e);
                }
            } // End of While - it.hasNext
        } // End of While - active

        try {
            closeSelector();
        } catch( Throwable e ) {}

        interrupt();
    } // end of startup ...

    public String getRemoteIPAddress() {
        return null;
    }

    public Socket getCurrentSocket() {
    	return null;
    }

    public boolean idle(long timeout) {
        return false;
    }

    public boolean connect() {
    	return true;
    }

    public void disconnect() {
    }


    public int getCurrentState() {
        return CommonLib.PROCESSING_PROTOCOL;
    }

    public boolean isConnected() {
        return true;
    }

    public void checkActivity() {
    }

    public synchronized void shutdown() {

        active    = false;
        if ( threadPool != null ) {
	        for ( int i=0; i < threadPool.size(); i++ ) {
                try {
                    SocketService service = (SocketService)threadPool.get(i);

                    if ( getTraceLevel() >= DiagLogger.INFO ) {
                        try {
                            logger.info( CommonLib.getMessage("BICEAIMSA031", new String[]{ context.getAdapterName(), this.getSocketInfo( service.getCurrentSocket() )} ) );
                        } catch( Exception e ) {}
                    }
                    service.shutdown();
                } catch( Throwable e ) {logger.error( e.getMessage(), e);}
	        }
        }

        threadPool = null;
        try {
            closeSelector();
        } catch (Throwable t) {logger.error( t.getMessage(), t);}

        try {
            serverSocket.close();
        } catch (Throwable t) {logger.error( t.getMessage(), t);}

        SocketAdapterManager.getInstance().changeAdapterStatus( this, false );
        //*interrupt();
    }
	/**
	 * @return Returns the context.
	 */
	public ConfigurationContext getContext() {
		return context;
	}
	/**
	 * @return Returns the active.
	 */
	public boolean isActive() {
		return active;
	}

    public void notifyMessage() {
    }

    public void setControl(Object control) {
    }

    public void setLastActivity() {
    }

    private String  getSocketInfo(SocketChannel channel) {
        StringBuffer connInfo = new StringBuffer();

        connInfo.append("Local Address=");
        connInfo.append( channel.socket().getLocalAddress().getHostAddress()).append(":").append(channel.socket().getLocalPort()).append(", ");
        connInfo.append("Remote Address=");
        connInfo.append( channel.socket().getInetAddress().getHostAddress()).append(":").append(channel.socket().getPort());

        return connInfo.toString();
    }

    private String  getSocketInfo(Socket socket) {
        StringBuffer connInfo = new StringBuffer();

        if ( socket != null ) {
            connInfo.append("Local Address=");
            connInfo.append( socket.getLocalAddress().getHostAddress()).append(":").append(socket.getLocalPort()).append(", ");
            connInfo.append("Remote Address=");
            connInfo.append( socket.getInetAddress().getHostAddress()).append(":").append(socket.getPort());
        }else {
            connInfo.append("SOCKET IS NULL");
        }

        return connInfo.toString();
    }

    private boolean isForInbound() {
    	return ( this.context.getBoundUsage().equals( ConfigurationContext.INBOUND_SOCKET ));
    }

    private boolean isForIobound() {
    	return ( this.context.getBoundUsage().equals( ConfigurationContext.IOBOUND_SOCKET ));
    }

    private boolean isForOutbound() {
    	return ( this.context.getBoundUsage().equals( ConfigurationContext.OUTBOUND_SOCKET ));
    }

    public synchronized void quiesceShutdown(SocketService service) {
        if ( threadPool == null  ) return;

        for ( int i=0; i < threadPool.size(); i++) {
            SocketService aService = (SocketService) threadPool.elementAt( i );
            if ( service !=  aService ) continue;
            //service.shutdown();
            if ( context.getConnLimitPerIp() > 0 ) {
                String ip = service.getRemoteIPAddress();
                if ( ip != null ) {
                    Integer	connCount 	= (Integer)ipTable.get( ip );
                    if ( connCount != null ) {
                        int newCount = connCount.intValue() - 1;
                        if ( newCount < 0 ) newCount = 0;

                        ipTable.put( ip, new Integer( newCount ) );
                    }
                }
            }

            threadPool.remove(i);
            break;
        }
        // Adapter status 변경
        if ( threadPool == null || threadPool.size() == 0 ) {
            SocketAdapterManager.getInstance().changeAdapterStatus( this, false );
        }
    }

    private void openSelector() throws IOException {

        serverSocketChannel = ServerSocketChannel.open();
        serverSocket        = serverSocketChannel.socket();
        selector            = Selector.open();

        if( context.getHostName() == null || context.getHostName().equals("*") || context.getHostName().equals("") || context.getHostName().equals("localhost")
        	|| context.getHostName().equals("0.0.0.0") || context.getHostName().equals("127.0.0.1") ) {
            serverSocket.bind(new InetSocketAddress(context.getPortNumber()), 1024);
            if ( getTraceLevel() >= DiagLogger.INFO ) {
                logger.info( CommonLib.getMessage("BICEAIMSA032", new String[]{ context.getAdapterName(), context.getHostName(), Integer.toString(context.getPortNumber()) }) );
            }
        } else {
            serverSocket.bind(new InetSocketAddress(context.getHostName(), context.getPortNumber()), 1024);
            if ( getTraceLevel() >= DiagLogger.INFO ) {
                logger.info( CommonLib.getMessage("BICEAIMSA032", new String[]{ context.getAdapterName(), context.getHostName(), Integer.toString(context.getPortNumber()) }) );
            }
        }

        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        synchronized ( threadPool ) {
            if ( threadPool.size() == 0 ) {
                SocketAdapterManager.getInstance().changeAdapterStatus( this, false );
            }
        }
    }

    private void closeSelector() throws IOException {

        if(selector != null) {
            for(Iterator<SelectionKey> iterator = selector.keys().iterator(); iterator.hasNext();)
                try {
                    SelectionKey selectionkey = iterator.next();
                    ServerSocketChannel serversocketchannel = (ServerSocketChannel)selectionkey.channel();
                    selectionkey.cancel();
                    serversocketchannel.socket().close();
                    serversocketchannel.close();
                    iterator.remove();
                }catch(UnsupportedOperationException e) { 
                	// Do nothing!!!
                }catch(IOException e) { 
                	logger.error( e.getMessage(), e);
                }catch(Exception e) {
                	logger.error( e.getMessage(), e);
                }
                selector.close();
        }
    } // end of closeSelector...

	public void disconnect(String localHostname, int localPortNumber)
		throws SocketAdapterException {
	}

	public void setContext(ConfigurationContext context) {
		this.context = context;
	}

    public long getFirstActivity() {
        return 0;
    }

    public long getLastActivity() {
        return 0;
    }
}
