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

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

import com.eactive.eai.adapter.EAIException;
import com.eactive.eai.adapter.socket2.common.CommonLib;
import com.eactive.eai.adapter.socket2.common.SMSLogger;
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.adapter.socket2.protocol.ProtocolHandler;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.ByteCharSequence;

public class DynamicOffsetDecoder extends ProtocolDecoderAdapter  {
	protected final Logger logger;
	protected final Logger errLogger;
	
    int hdrLength = 0;//패턴때문에 보정필요
	public static final int MAX_PAYLOAD_SIZE = 25 * 1024 * 1024;   // 25MB
	
	private static final String ERROR_CODE = "RECEAINNA008";
	
	private static final String DECODER_STATE_KEY = DynamicOffsetDecoder.class.getName() + ".STATE";

	private enum State {
		STATE_HEADER_READ, STATE_PAYLOAD_READ;
	}

	private static class CodecState {
		State       state = State.STATE_HEADER_READ; 
		ByteBuffer 	buffer = ByteBuffer.allocate(0).setAutoExpand(true);
		int      	payloadSize;
		int      	offsetSize;
	}
	
	SocketAdapterContext context;
	ISocketAdapter adapter;

	public DynamicOffsetDecoder(ISocketAdapter adapter) {
		this.adapter = adapter;
		this.context = adapter.getContext();

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

	@SuppressWarnings("static-access")
	public void decode(IoSession session, ByteBuffer in, ProtocolDecoderOutput out) throws Exception {
		CodecState codecState = (CodecState) session.getAttribute(DECODER_STATE_KEY);
		if (codecState == null) {
			codecState = new CodecState();
			codecState.buffer.limit(context.getLlFieldOffset() + context.getLlFieldLength() + hdrLength);
			session.setAttribute(DECODER_STATE_KEY, codecState);
		}

		if ( context.getTraceLevel() >= context.TRACE_LEVEL_TRACE ) {
			StringBuilder sb = new StringBuilder();
			sb.append("DynamicOffsetDecoder");
			//sb.append(codecState.state);
			if (codecState.state == State.STATE_PAYLOAD_READ) {
				sb.append("(" + codecState.payloadSize + ")");
			}
			sb.append(" | decode : " + CommonLib.hexDump(in, context));
			logger.debug(sb.toString());
			adapter.addRecentRecvTrace(session, sb.toString());
		}

		while(in.hasRemaining()) {
			session.setIdleTime(IdleStatus.BOTH_IDLE, context.getTimeout());

			switch (codecState.state) {
			case STATE_HEADER_READ:		
				if (readHeader(session, codecState, in)) {
					
					/**
					 * payload size < 0 인 경우
					 */
					if (codecState.payloadSize < 0) {
						throw new EAIException(ERROR_CODE, context.getAdapterName() +
//								" (" + context.getLlFieldType() + "," + context.getLlFieldOffset() + "," + context.getLlFieldLength() +
								" (" + context.getLlFieldType() + "," + codecState.offsetSize + "," + context.getLlFieldLength() +
								") | when read header, ll length field is invalid | " + CommonLib.hexDump(codecState.buffer, context));						
					/**
					 * payload size == 0 인 경우
					 */
					} else if (codecState.payloadSize == 0) {
						if (context.isSync()) {
							throw new Exception("<RECV> payload size 0 (ping). 'SYNC' response type socket does not support ping(size 0) message.");
						}

			 			//codecState.state = State.STATE_HEADER_READ;
						session.setAttribute(ProtocolHandler.CODEC_STATE, codecState.state.toString());

			 			if ( context.getTraceLevel() >= context.TRACE_LEVEL_INFO ) {
							logger.info(context.getAdapterName() + " | <RECV> payload size 0 (ping) -> ignore. - " + CommonLib.hexDump(in, context));
							adapter.addRecentRecvTrace(session, "payload size 0 (ping) -> ignore. - " + CommonLib.hexDump(in, context));
						}

			 			/**
						 * header 값을 읽은 후 buffer의 position을 이동해 준다.
						 * 이 작업을 안해주면 무한루프에 빠질 수 있다.
						 * 예) 법원 공탁금 (_LAW_IN_SOC_AsS{...})
						 */
						codecState.buffer.position(0).limit(context.getLlFieldOffset() + context.getLlFieldLength());//초기화
						
						if (context.isClient() && context.isPermanent() && context.isPollingUse()) {
							session.setIdleTime(IdleStatus.BOTH_IDLE, context.getPollingTimeout());
						} else {
							session.setIdleTime(IdleStatus.BOTH_IDLE, context.getIdleTimeout());
						}

						break;
					}
					
					/**
					 * ll.include = 'Y' - LL을 포함해서 넘긴다.
					 */
					if ("Y".equals(context.getLlInclude())) {
						try {
							codecState.buffer.position(context.getLlFieldOffset() + context.getLlFieldLength())
			                   .limit(codecState.payloadSize + context.getLlFieldLength());
//							if (codecState.offsetSize == context.getLlFieldOffset()){ //일반전문
//								codecState.buffer.position(codecState.offsetSize + context.getLlFieldLength() ) 
//								.limit(codecState.payloadSize + context.getLlFieldLength());
//							}else{//폴링전문
//								codecState.buffer.position(codecState.offsetSize +context.getLlFieldOffset() + context.getLlFieldLength() )
//								.limit(codecState.payloadSize + context.getLlFieldLength());
//							}
						} catch(IllegalArgumentException e) {
							StringBuilder sb = new StringBuilder();
							sb.append(context.getAdapterName() + " | error when handling buffer | ");							
//							sb.append("offset=" + context.getLlFieldOffset() + context.getLlFieldLength());
							sb.append("offset=" + codecState.offsetSize + context.getLlFieldLength());
							sb.append(", limit=" + codecState.payloadSize + context.getLlFieldLength());
							logger.error(sb.toString());
			    			errLogger.error(sb.toString());
			    			adapter.addRecentError(sb.toString());
//			    			SMSLogger.loggingSMSWithCode(context.getAdapterName(), "RECEAINNA007");
			    			SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA007");
							throw e;
						}
					/**
					 * ll.include = 'N' - LL을 제외하고 넘긴다.
					 */
					} else {
						try {
							codecState.buffer.position(context.getLlFieldOffset()).limit(codecState.payloadSize);
//							if (codecState.offsetSize == context.getLlFieldOffset()){ //일반전문
//								codecState.buffer.position(codecState.offsetSize).limit(codecState.payloadSize);
//							}else{
//								codecState.buffer.position(codecState.offsetSize + context.getLlFieldOffset()).limit(codecState.payloadSize);
//							}
						} catch(IllegalArgumentException e) {
							StringBuilder sb = new StringBuilder();
							sb.append(context.getAdapterName() + " | error when handling buffer | ");
//							sb.append("offset=" + context.getLlFieldOffset());
							sb.append("offset=" + codecState.offsetSize);
							sb.append(", limit=" + codecState.payloadSize);
							logger.error(sb.toString());
			    			errLogger.error(sb.toString());
			    			adapter.addRecentError(sb.toString());
			    			SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA007");
							throw e;
						}
					}
					
					/**
					 * offset + ll 을 읽었고, 더 이상 읽을 것(payload)이 없는 경우
					 * - ll.length value = ll.offset + ll.field.length
					 */
					if (codecState.buffer.position() == codecState.buffer.limit()) {
						byte[] bytes = new byte[codecState.buffer.limit()];
						codecState.buffer.position(0);
						codecState.buffer.get(bytes);
						ByteMessage message = new ByteMessage(bytes);
						out.write(message);
						codecState.buffer.position(0).limit(context.getLlFieldOffset() + context.getLlFieldLength());//초기화
						
						if (context.isClient() && context.isPermanent() && context.isPollingUse()) {
							session.setIdleTime(IdleStatus.BOTH_IDLE, context.getPollingTimeout());
						} else {
							session.setIdleTime(IdleStatus.BOTH_IDLE, context.getIdleTimeout());
						}
						
						break;
					}
						
					codecState.state = State.STATE_PAYLOAD_READ;
					session.setAttribute(ProtocolHandler.CODEC_STATE, codecState.state.toString());
				}
				break;
			case STATE_PAYLOAD_READ:
				if (readPayload(session, codecState, in)) {
					byte[] bytes = new byte[codecState.buffer.limit()];
					codecState.buffer.position(0);
					codecState.buffer.get(bytes);
//					ByteMessage message = new ByteMessage(bytes);
					ByteMessage message = null;
					if (codecState.offsetSize == context.getLlFieldOffset()){
						message = removeData(new ByteMessage(bytes));
					}else{
						message = new ByteMessage(bytes);
					}
					
					out.write(message);
					codecState.buffer.position(0).limit(context.getLlFieldOffset() + context.getLlFieldLength());//limit 초기화
					if (context.isClient() && context.isPermanent() && context.isPollingUse()) {
						session.setIdleTime(IdleStatus.BOTH_IDLE, context.getPollingTimeout());
					} else {
						session.setIdleTime(IdleStatus.BOTH_IDLE, context.getIdleTimeout());
					}
		 			codecState.state = State.STATE_HEADER_READ;
					session.setAttribute(ProtocolHandler.CODEC_STATE, codecState.state.toString());
				}
				
				break;
			}
			
			session.setAttribute(ProtocolHandler.CODEC_STATE_BUFFER, CommonLib.hexDumpRead(codecState.buffer, context));
		}
	}

	private boolean readHeader(IoSession session, CodecState codecState, ByteBuffer in) throws Exception {
		/*
		if ( context.getTraceLevel() >= context.TRACE_LEVEL_TRACE ) {
			logger.debug(context.getAdapterName() + " | read header : " + in);
		}
		*/

        if (codecState.buffer.remaining() > in.remaining()) {
        	codecState.buffer.put(in);
            return false;
        }

    	while(codecState.buffer.hasRemaining()) {
    		codecState.buffer.put(in.get());
    	}
    	//offsetSize 를 가져온다.
    	codecState.offsetSize = getPatternOffset(codecState);
		if ("CHAR".equals(context.getLlFieldType())) {
			codecState.buffer.flip();
			try {
				byte[] header = new byte[context.getLlFieldLength()];				
				for (int i=0; i<context.getLlFieldLength(); i++) {
//					header[i] = codecState.buffer.get(context.getLlFieldOffset() + i);
					header[i] = codecState.buffer.get(codecState.offsetSize + i);
				}
				codecState.payloadSize = Integer.parseInt(new String(header));
			} catch (NumberFormatException e) {
//				SMSLogger.loggingSMSWithCode(context.getAdapterName(), ERROR_CODE);
				SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), ERROR_CODE);
				throw new EAIException(ERROR_CODE, context.getAdapterName() +
//						" (" + context.getLlFieldType() + "," + context.getLlFieldOffset() + "," + context.getLlFieldLength() 
						" (" + context.getLlFieldType() + "," + codecState.offsetSize + "," + context.getLlFieldLength() 
						+ ") | when read header, number format is invalid - " 
						+ e.getMessage() + " | " + CommonLib.hexDump(codecState.buffer, context));
			}
		}
		
		if (codecState.payloadSize > MAX_PAYLOAD_SIZE) {
			throw new Exception("max payload size(" + MAX_PAYLOAD_SIZE + ") exceeded : " + 
					            codecState.payloadSize + " | " + 
					            CommonLib.hexDump(codecState.buffer, context));
		}
		
		/**
		 * ll.field.include = 'Y' - 헤더 길이를 빼 준다.
		 */
		if ("Y".equals(context.getLlFieldInclude())) {
			codecState.payloadSize = codecState.payloadSize - context.getLlFieldLength();
		}
		
		/**
		 * ll.offset.exclude = 'Y' - offset 만큼의 크기를 더해서 length를 계산한다.
		 */
		if ("Y".equals(context.getLlOffsetExclude())) {
//			codecState.payloadSize = codecState.payloadSize + context.getLlFieldOffset();
			codecState.payloadSize = codecState.payloadSize + codecState.offsetSize;
		}
		
		/**
		 * ll.length.adjust = 200 - 공통부 header 길이를 더한다. (한은망 평문 테스트용)
		 */
		if (context.getLlLenAdjust() != 0) {
			codecState.payloadSize = codecState.payloadSize + context.getLlLenAdjust();
		}
		return true;
	}

	private boolean readPayload(IoSession session, CodecState codecState, ByteBuffer in) throws Exception {
		/*
		if ( context.getTraceLevel() >= context.TRACE_LEVEL_TRACE ) {
			logger.debug(context.getAdapterName() + " | read payload : " + in);
		}
		*/

        if (codecState.buffer.remaining() > in.remaining()) {
        	codecState.buffer.put(in);
            return false;
        }
        
    	while(codecState.buffer.hasRemaining()) {
    		codecState.buffer.put(in.get());
    	}
		
		return true;
	}
	private int getPatternOffset(CodecState codecState){
		int offset = 0;
		
		byte[] data = new byte[codecState.buffer.limit()];
    	codecState.buffer.flip();
    	codecState.buffer.get(data);
    	
    	String dynamicOffsetPattern = context.getDynamicOffsetPattern().trim();
    	Matcher matcher = Pattern.compile(dynamicOffsetPattern).matcher(new ByteCharSequence(data));
    	if(matcher.find()){
    		offset = matcher.start();
    	}else{
    		offset = context.getLlFieldOffset();
    	}
    	return offset;
	}
	private ByteMessage removeData(ByteMessage m) throws Exception{
		ByteMessage mm = null ;
		
		/**
		 * removedata.rule
		 */
		if (!"".equals(context.getDataRemoveRule())) {
			Matcher matcher = Pattern.compile("\\[([ ]*[0-9]+)[ ]*,([ ]*[0-9]+)[ ]*\\][ ]*").matcher(context.getDataRemoveRule().trim());
			
			if (matcher.find()) {
			    int offset = Integer.parseInt(matcher.group(1).trim());
			    int length = Integer.parseInt(matcher.group(2).trim());
				mm = (new ByteMessage())
		                .append(m.getBytes(0, offset))
		                .append(m.getBytes(offset + length));
			} 
			else {
				throw new Exception(context.getAdapterName() + " | data.remove.rule format is invalid : " + context.getDataRemoveRule());
			}
		}
		return mm;
	}
}
