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

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.SocketAdapter;
import com.eactive.eai.adapter.socket2.protocol.fixedheader.codec.FixedHeaderDecoder;
import com.eactive.eai.common.util.Logger;

import java.io.*;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FixedHeaderBlockingHandler {
	@SuppressWarnings("static-access")
	static public byte[] sendMessage(SocketAdapter adapter, Object request) throws Exception {
		SocketAdapterContext context = adapter.getContext();
		Logger logger = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());

		byte[] m = null;
		if (request instanceof byte[]) {
			m = (byte[]) request;
		} else if (request instanceof String) {
			m = ((String) request).getBytes();
		}
		if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
			logger.debug(context.getAdapterName() + " | <SEND> " + CommonLib.hexDump(m, true, context));
			adapter.addRecentSend(null, CommonLib.hexDump(m, true, context));
		}

		Socket socket = null;
		InputStream is = null;
		OutputStream os = null;

		// System.out.println(Thread.currentThread().getName() +
		// " ### before get | semaphore + " +
		// context.getMaxConnSemaphore().availablePermits());
		boolean success = context.getMaxConnSemaphore().tryAcquire(context.getTimeout(),
				TimeUnit.SECONDS);
		if (!success) {
			throw new TimeoutException("connot get max conn semaphore - "
					+ context.getMaxConnection());
		}

		try {
			socket = new Socket(context.getHostName(), context.getPortNumber());
			socket.setSoTimeout(context.getTimeout() * 1000);
			is = new BufferedInputStream(socket.getInputStream());
			os = new BufferedOutputStream(socket.getOutputStream());
			os.write(encode(context, m));
			os.flush();
			adapter.incMessageSend();

			byte[] response = decode(context, m, socket, is);
			adapter.incMessageRecv();

			if (context.getTraceLevel() >= context.TRACE_LEVEL_DEBUG) {
				logger.debug(context.getAdapterName() + " | <RECV> " + CommonLib.hexDump(response, true, context));
				adapter.addRecentRecv(null, CommonLib.hexDump(response, true, context));
			}

			return response;
		} finally {
			if (is != null)
				try {
					is.close();
				} catch (Exception e) {
				}
			if (os != null)
				try {
					os.close();
				} catch (Exception e) {
				}
			if (socket != null)
				try {
					socket.close();
				} catch (Exception e) {
				}
			context.getMaxConnSemaphore().release();
			// System.out.println(Thread.currentThread().getName() +
			// " ### after release | semaphore - " +
			// context.getMaxConnSemaphore().availablePermits());
		}
	}

	@SuppressWarnings("static-access")
	static private byte[] encode(SocketAdapterContext context, byte[] request) throws Exception {
		Logger logger = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());

		ByteMessage m = new ByteMessage(request);
		ByteMessage result = new ByteMessage();

		/**
		 * ll.include = 'Y' - 통째로 보낸다.
		 */
		if ("Y".equals(context.getLlInclude())) {
			ByteMessage temp = new ByteMessage();
			temp.append(m.getBytes(0, context.getLlFieldOffset()));
			temp.append(m.getBytes(context.getLlFieldOffset() + context.getLlFieldLength()));
			m = temp;
		}

		int length = m.size();

		/**
		 * ll.field.include = 'Y' - header 길이 포함하여 ll 을 계산한다.
		 */
		if ("Y".equals(context.getLlFieldInclude())) {
			length = length + context.getLlFieldLength();
		}

		/**
		 * ll.offset.exclude = 'Y' - offset 만큼의 크기를 빼서 length를 계산한다.
		 */
		if ("Y".equals(context.getLlOffsetExclude())) {
			length = length - context.getLlFieldOffset();
		}

		/**
		 * [1] before-payload
		 */
		result.append(m.getBytes(0, context.getLlFieldOffset()));

		/**
		 * [2] write header
		 */
		byte[] header = null;
		if ("BIN".equals(context.getLlFieldType())) {
			int tmpLength = length;
			header = new byte[context.getLlFieldLength()];
			for (int i = context.getLlFieldLength() - 1; i >= 0; i--) {
				header[i] = (byte) (tmpLength % 256);
				tmpLength = tmpLength / 256;
			}
		} else { // "CHAR"
			String lenStr = CommonLib.getFormatString(length, context.getLlFieldLength());
			header = lenStr.getBytes();
		}
		result.append(header);

		/**
		 * [3] after-payload
		 */
		result.append(m.getBytes(context.getLlFieldOffset(), m.getBytes().length
				- context.getLlFieldOffset()));

		if (context.getTraceLevel() >= context.TRACE_LEVEL_TRACE) {
			logger.debug(context.getAdapterName() + " | FixedHeaderEncoder(Blocking) | encode : "
					+ result.hexDump());
		}
		return result.getBytes();
	}

	@SuppressWarnings("static-access")
	static private byte[] decode(SocketAdapterContext context, byte[] request, Socket socket,
			InputStream is) throws Exception {
		Logger logger = SocketLoggerManager.getInstance().getLogger(context.getAdapterGroupName());

		ByteMessage result = new ByteMessage();

		byte[] offsetPlusHeader = new byte[context.getLlFieldOffset() + context.getLlFieldLength()];
		int read = is.read(offsetPlusHeader);
		if (read == -1) {
			throw new IOException("socket is closed by peer - "
					+ socket.getLocalAddress().getHostName() + ":" + socket.getLocalPort() + "->"
					+ context.getHostName() + ":" + socket.getPort());
		}

		int payloadSize = 0;
		if ("BIN".equals(context.getLlFieldType())) {
			for (int i = 0; i < context.getLlFieldLength(); i++) {
				payloadSize += ((int) offsetPlusHeader[context.getLlFieldOffset() + i] & 0xFF)
						* Math.pow(256, context.getLlFieldLength() - i - 1);
			}
		} else {
			try {
				byte[] header = new byte[context.getLlFieldLength()];
				for (int i = 0; i < context.getLlFieldLength(); i++) {
					header[i] = offsetPlusHeader[context.getLlFieldOffset() + i];
				}
				payloadSize = Integer.parseInt(new String(header));
			} catch (NumberFormatException e) {
//				SMSLogger.loggingSMSWithCode(context.getAdapterName(), "RECEAINNA008");
				SMSLogger.loggingSMS4Soc(context.getAdapterName(), context.getDescription(), "RECEAINNA008");
				throw new EAIException("RECEAINNA008", context.getAdapterName() + " ("
						+ context.getLlFieldType() + "," + context.getLlFieldOffset() + ","
						+ context.getLlFieldLength()
						+ ") | when read header, number format is invalid - " + e.getMessage()
						+ " | " + CommonLib.hexDump(offsetPlusHeader, true, context));
			}
		}

		if (payloadSize > FixedHeaderDecoder.MAX_PAYLOAD_SIZE) {
			throw new Exception("max payload size(" + FixedHeaderDecoder.MAX_PAYLOAD_SIZE
					+ ") exceeded : " + payloadSize);
		}

		/**
		 * ll.field.include = 'Y' - 헤더 길이를 빼 준다.
		 */
		if ("Y".equals(context.getLlFieldInclude())) {
			payloadSize = payloadSize - context.getLlFieldLength(); // -
			// context.
			// getLlFieldOffset
			// ();
		}

		/**
		 * ll.offset.exclude = 'Y' - offset 만큼의 크기를 더해서 length를 계산한다.
		 */
		if ("Y".equals(context.getLlOffsetExclude())) {
			payloadSize = payloadSize + context.getLlFieldOffset();
		}

		/**
		 * ll.field.include = 'Y' - 헤더 길이를 빼 준다.
		 */
		if ("Y".equals(context.getLlInclude())) {
			result.append(offsetPlusHeader);
		} else {
			result.append((new ByteMessage(offsetPlusHeader)).getBytes(0, context
					.getLlFieldOffset()));
		}

		byte[] remaining = readPayload(is, payloadSize - context.getLlFieldOffset());
		result.append(remaining);

		ByteMessage full = (new ByteMessage(offsetPlusHeader)).append(remaining);
		if (context.getTraceLevel() >= context.TRACE_LEVEL_TRACE) {
			logger.debug(context.getAdapterName() + " | FixedHeaderDecoder(Blocking) | decode : "
					+ full.hexDump());
		}

		return result.getBytes();
	}

	private static byte[] readPayload(InputStream in, int total) throws Exception {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		int amt = 0;
		byte[] buf = new byte[2048];
		while (amt < total) {
			int n = in.read(buf, 0, total-amt<2048 ? total-amt : 2048);
			if (n == -1) {
				break;
			}
			amt += n;
			bos.write(buf, 0, n);
		}
		bos.flush();
		return bos.toByteArray();
	}
}
