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

import com.eactive.eai.adapter.socket2.common.CommonLib;
import com.eactive.eai.adapter.socket2.common.SimpleFuture;
import com.eactive.eai.adapter.socket2.config.SocketAdapterContext;
import com.eactive.eai.adapter.socket2.config.SocketLoggerManager;
import com.eactive.eai.adapter.socket2.protocol.ByteMessage;
import com.eactive.eai.adapter.socket2.protocol.ISocketAdapter;
import com.eactive.eai.common.util.Logger;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoFilterAdapter;
import org.apache.mina.common.IoSession;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


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

	private static final String FILTER_STATE_KEY = PollingFilter.class.getName() + ".STATE";
	
	private static class FilterState {
		SimpleFuture pollingResponseFuture = new SimpleFuture();
	}
	
	protected SocketAdapterContext context;
	protected ISocketAdapter adapter;
	protected ExecutorService executor;
	
	
	public PollingFilter(ISocketAdapter adapter) {
		this.adapter = adapter;
		this.context = adapter.getContext();
		this.executor = Executors.newCachedThreadPool();
		((ThreadPoolExecutor) this.executor).setKeepAliveTime(1L, TimeUnit.SECONDS);

	    logger    = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());
	    errLogger = SocketLoggerManager.getInstance().getErrLogger();
	}
	
	
	@Override
	public void sessionCreated(NextFilter nextFilter, IoSession session) throws Exception {
		FilterState filterState = (FilterState) session.getAttribute(FILTER_STATE_KEY);
		if (filterState == null) {
			filterState = new FilterState();
			//SimpleFuture handshakeFuture = new SimpleFuture();
			session.setAttribute(FILTER_STATE_KEY, filterState);
		}
		
		nextFilter.sessionCreated(session);
	}
	
	
	@Override
	public void sessionClosed(NextFilter nextFilter, IoSession session) throws Exception {
		FilterState filterState = (FilterState) session.getAttribute(FILTER_STATE_KEY);
		if (filterState != null && filterState.pollingResponseFuture != null) {
			filterState.pollingResponseFuture.set(new Exception("session closed by peer"));
		}
		
		nextFilter.sessionClosed(session);
	}
	
	
	@Override
    public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
		int responseTimeout = 0;
		
		if (session.containsAttribute("responseTimeout")){
			responseTimeout = (Integer)session.getAttribute("responseTimeout");
		}
//		if (context.isPollingSender() && context.isPermanent() && context.isPollingUse()
//			&& System.currentTimeMillis()-session.getLastIoTime() > (context.getTimeout() + 1) * 1000L
//	    	&& System.currentTimeMillis()-session.getLastIoTime() < (context.getPollingTimeout() + 10) * 1000L) {
//	    	sendPollingRequest(session);
//	    } else {
//			nextFilter.sessionIdle(session, status);
//	    }
		if (responseTimeout > 0){
			nextFilter.sessionIdle(session, status);
		}else{//원래로직
			if (context.isPollingSender() && context.isPermanent() && context.isPollingUse()
				&& System.currentTimeMillis()-session.getLastIoTime() > (context.getTimeout() + 1) * 1000L
		    	&& System.currentTimeMillis()-session.getLastIoTime() < (context.getPollingTimeout() + 10) * 1000L) {
		    	sendPollingRequest(session);
		    } else {
				nextFilter.sessionIdle(session, status);
		    }			
		}

    }
	

	@Override
    public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) throws Exception {
		FilterState filterState = (FilterState) session.getAttribute(FILTER_STATE_KEY);
		if (filterState != null && filterState.pollingResponseFuture != null) {
			filterState.pollingResponseFuture.set(cause);
		}
        nextFilter.exceptionCaught(session, cause);
    }
	
	
	@Override
	public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
		FilterState filterState = (FilterState) session.getAttribute(FILTER_STATE_KEY);

		ByteMessage m = (ByteMessage) message;

		if (recvPolling(session, m, filterState)) {
			if (filterState.pollingResponseFuture == null) {		
				filterState.pollingResponseFuture = new SimpleFuture();
			}				
			filterState.pollingResponseFuture.set(m);
		} else {
			nextFilter.messageReceived(session, message);
		}
	}
	
	
	private void sendPollingRequest(IoSession session) {
		FilterState filterState = (FilterState) session.getAttribute(FILTER_STATE_KEY);
		
		/**
		 * send polling request
		 */
		final byte[] pollingRequest = getPollingRequestFormatted();
		
		if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
		    adapter.addRecentSend(session, "PollingFilter | session idle -> <SEND> polling request - " 
				    + CommonLib.hexDump(pollingRequest, true, context));
			logger.debug(context.getAdapterName() + " | PollingFilter | session idle -> <SEND> polling request - " 
					    + CommonLib.hexDump(pollingRequest, true, context));
		}	
		
		IoSession sess = adapter.getSessionPool().acquire(session);
		
		try {
			if (filterState.pollingResponseFuture == null) {				
				filterState.pollingResponseFuture = new SimpleFuture();
			}
		
			final IoSession s = sess;
			final FilterState hState = filterState;
				
			FutureTask futureTask = new FutureTask(new Callable() {
				public byte[] call() {
					if (Thread.currentThread().getName().startsWith("pool-")) {
						Thread.currentThread().setName(context.getAdapterName() + "-PollingFilter-" + Thread.currentThread().getName());
					}

					try {		
						internalSendMessage(s, pollingRequest);
						
						/**
						 * oneway 이면 기다리지 않는다.
						 */
						if (adapter.getContext().isPollingOneway()) {
							return null;
						}
		
						if (hState.pollingResponseFuture != null) {				
							Object result = hState.pollingResponseFuture.join(context.getTimeout() * 1000L);
							if (result instanceof ByteMessage) {
								return ((ByteMessage) result).getBytes();
							} else {
								throw new Exception("error when wait polling response", (Throwable) result);
							}
						} else {
							throw new Exception("future result(poll message response) is null");
						}
					} catch(Exception e) {
						adapter.addRecentError(context.getAdapterName() + " | " + e.getMessage(), e);
						logger.warn(context.getAdapterName() + " | " + e.getMessage());
						return null;
					}
				}
			});					
						
			executor.execute(futureTask);		
		} catch(Exception e) {
    		adapter.addRecentError(context.getAdapterName() + " | PollingFilter | error when send POLLING request - " + e.getMessage(), e);
			logger.warn(context.getAdapterName() + " | PollingFilter | error when send POLLING request - " + e.getMessage());
		} finally {
			if (sess != null) {
				adapter.getSessionPool().release(sess);
			}
		}
	}
	
	
	private boolean recvPolling(IoSession session, ByteMessage m, FilterState filterState) throws Exception {
		ByteMessage om = m;
		
		if ("Y".equals(context.getLlInclude())) {
			m = (new ByteMessage())
			    .append(m.getBytes(0, context.getLlFieldOffset()))
			    .append(m.getBytes(context.getLlFieldOffset() + context.getLlFieldLength()));
		}

		if (context.isPollingReceiver() && m.getString().matches(context.getPollingRequestPattern())) {
			if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
			    adapter.addRecentRecv(session, "PollingFilter | <RECV> polling request - " + om.hexDump());
				logger.debug(context.getAdapterName() + " | PollingFilter | <RECV> polling request - " + om.hexDump());
			}
			
			/**
			 * oneway 일 경우, polling 응답을 보내지 않는다
			 */
			if (context.isPollingOneway()) {
				if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
				    adapter.addRecentRecv(session, "PollingFilter | oneway polling - skip sending polling response");
					logger.debug(context.getAdapterName() + " | PollingFilter | oneway polling - skip sending polling response");
				}
				return true;
			}

			try {
				byte[] response = m.getString().replaceAll(context.getPollingRequestPattern(), 
                                                           context.getPollingReplacement()).getBytes();
				
				if ("Y".equals(context.getLlInclude())) {
					byte[] ll = new byte[context.getLlFieldLength()];
					ByteMessage rm = new ByteMessage(response);
					ByteMessage mm = (new ByteMessage())
					                 .append(rm.getBytes(0, context.getLlFieldOffset()))
					                 .append(ll)
					                 .append(rm.getBytes(context.getLlFieldOffset()));
					response = mm.getBytes();
				}
				
				if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
				    adapter.addRecentSend(session, "PollingFilter | <SEND> polling response - " + CommonLib.hexDump(response, true, context));
					logger.debug(context.getAdapterName() + " | PollingFilter | <SEND> polling response - " + CommonLib.hexDump(response, true, context));
				}
				
				internalSendMessage(session, response);
			} catch (Exception e) {
				throw e;
			}
			
			return true;
		} else if (context.isPollingSender() && m.getString().matches(context.getPollingResponsePattern())) {
			if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
			    adapter.addRecentRecv(session, "PollingFilter | <RECV> polling response - " + om.hexDump());
				logger.debug(context.getAdapterName() + " | PollingFilter | <RECV> polling response - " + om.hexDump());
			}
			
			return true;
		}
		
		return false;		
	}	
	
	
	protected void internalSendMessage(IoSession session, Object message) throws Exception {	
		ByteMessage m = null;
		
		if (message instanceof byte[]) {
			m = new ByteMessage((byte[]) message);
		} else if (message instanceof ByteMessage) {
			m = (ByteMessage) message;
		} else if (message instanceof String) {
			m = new ByteMessage((String) message);
		} else {
			throw new Exception("Unknown data format");
		}

//		WriteFuture future = session.write(message);
		session.write(message);
		//future.join(context.getTimeout() * 1000L);
	}
	
	
	protected byte[] getPollingRequestFormatted() {
		String message = context.getPollingRequest();
		//Polling 로직 수정  2017.05 KSH
		String str[] = message.split("%d");
		String endMsg = "";
		for(int i=0; i<str.length; i++){
			if(i<1){
				endMsg = str[i];
			}else{
				String newMsg = "%d"+str[i];
				Matcher matcher = Pattern.compile("%d\\{([a-zA-Z \\-]+)\\}").matcher(newMsg);
				if (matcher.find()) {
					String datePattern = matcher.group(1);
					DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(datePattern);
					endMsg = endMsg + newMsg.replaceAll("%d\\{([a-zA-Z \\-]+)\\}", dateFormat.format(ZonedDateTime.now()));
				}
			}
		}
		message = endMsg;
		
		
		if ("Y".equals(context.getLlInclude())) {		
			byte[] ll = new byte[context.getLlFieldLength()];    	
			ByteMessage mm = (new ByteMessage())
			                 .append(message.substring(0, context.getLlFieldOffset()))
			                 .append(ll)
			                 .append(message.substring(context.getLlFieldOffset()));
	    	return mm.getBytes();
		} else {
			return message.getBytes();
		}
	}	
}
