package com.eactive.eai.message;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.message.filter.MessageFilter;

public class StandardMessage extends StandardItem {
	private static final long serialVersionUID = 8250470942348479859L;
	static Logger logger = LoggerFactory.getLogger(StandardMessage.class);
	
	String eaiSvcCd = null;
	
	int readPosition = 0;
	
	private String serviceKey = null;
	
	private String bizDataCharset = null;
	
	private String bizDataPath = null;
	private String llDataPath = null;
	private String zzDataPath = null;

	private MessageFilter flatFilter = null;
	
	public String getEaiSvcCd() {
		return eaiSvcCd;
	}

	public void setEaiSvcCd(String eaiSvcCd) {
		this.eaiSvcCd = eaiSvcCd;
	}

	public String getBizDataCharset() {
		return bizDataCharset;
	}

	public void setBizDataCharset(String bizDataCharset) {
		this.bizDataCharset = bizDataCharset;
	}

	public MessageFilter getFlatFilter() {
		return flatFilter;
	}

	public void setFlatFilter(MessageFilter flatFilter) {
		this.flatFilter = flatFilter;
	}
	
	public String getServiceKey() {
		return serviceKey;
	}

	public void setServiceKey(String serviceKey) {
		this.serviceKey = serviceKey;
	}

	public String getBizDataPath() {
		return bizDataPath;
	}

	public void setBizDataPath(String bizDataPath) {
		this.bizDataPath = bizDataPath;
	}

	
	public String getLlDataPath() {
		return llDataPath;
	}

	public void setLlDataPath(String llDataPath) {
		this.llDataPath = llDataPath;
	}

	public String getZzDataPath() {
		return zzDataPath;
	}

	public void setZzDataPath(String zzDataPath) {
		this.zzDataPath = zzDataPath;
	}
	
	public String getBizData() throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item == null) {
			return null;
		}
		else {
			return item.getValue();
		}
	}
	
	public byte[] getBizDataBytes() throws Exception {
		if(StringUtils.isEmpty(this.bizDataCharset)) {
			throw new Exception("bizData Charset not defined or bizData not set yet."); 
		}
		return getBizDataBytes(this.bizDataCharset);
	}
	
	public byte[] getBizDataBytes(String charset) throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item == null) {
			return null;
		}		
		else {
			byte[] bizBytes = item.getBytesValue(charset);
			if(StringUtils.isNotEmpty(bizDataCharset) && bizBytes != null &&
				bizDataCharset.equalsIgnoreCase(charset)){
				return bizBytes;
			}
			else {
				String bizData = item.getValue();
				if(bizData != null) {
					bizDataCharset = charset;
					bizBytes = bizData.getBytes(charset);
					item.setBytesValue(bizBytes);
					return bizBytes;      	
				}
				else {
					return null;
				}
			}
		}
	}
	
	public void setBizData(byte[] data, String charset) throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item != null) {
			if (data == null){
				item.setValue(null);
				item.setBytesValue(null);
			}
			else{
				item.setBytesValue(data);
		        item.setValue(new String(data, charset));	        
			}
			setBizDataCharset(charset);
		}
	}
	
	public void setBizData(String data, String charset) throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item != null) {
			if (data == null){
				item.setValue(null);
				item.setBytesValue(null);
			}
			else{
				item.setValue(data);
				item.setBytesValue(data.getBytes(charset));
			}
			setBizDataCharset(charset);
		}
	}
	
	public void setBizData(Object data, String charset) throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item != null) {
			if (data == null){
				item.setValue(null);
				item.setBytesValue(null);
				setBizDataCharset(charset);
			}else if (data instanceof String){
				setBizData((String)data, charset);				
	        }else if (data instanceof byte[]){
	        	setBizData((byte[])data, charset);	        	
	        }else {
	        	throw new Exception("invalid data object. -" + data.getClass());
	        }
	        setBizDataCharset(charset);
		}
	}
	
	// use only FlatReader
	public void cutBizData(int zzSize) throws Exception {
		if(bizDataPath == null) {
			throw new Exception("bizDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(bizDataPath);
		if(item != null) {
			byte[] bizDataBytes = item.getBytesValue(this.bizDataCharset);
			int finalSize = bizDataBytes.length - zzSize;
			 
			if(finalSize > 0) {
				byte[] fbizDataBytes = new byte[finalSize];
				for(int i=0; i<finalSize; i++) {
					fbizDataBytes[i] = bizDataBytes[i];
				}
				item.setBytesValue(fbizDataBytes);
			}
		}
	}
	
	public String getLlData() throws Exception {
		if(llDataPath == null) {
			throw new Exception("llDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(llDataPath);
		if(item == null) {
			return null;
		}
		else {
			return item.getValue();
		}
	}
		
	public void setLlData(String llData) throws Exception {
		if(llDataPath == null) {
			throw new Exception("llDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(llDataPath);
		if(item != null) {
			item.setValue(llData);
		}
	}
	public void setLlData(int llDataSize) throws Exception {
		if(llDataPath == null) {
			throw new Exception("llDataPath path not configured, check layout definition.");
		}
		StandardItem bizItem = findItem(llDataPath);
		if(bizItem != null) {
			bizItem.setValue(Integer.toString(llDataSize));
		}
	}
	
	public String getZzData() throws Exception {
		if(zzDataPath == null) {
			throw new Exception("zzDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(zzDataPath);
		if(item == null) {
			return null;
		}
		else {
			return item.getValue();
		}
	}
		
	public void setZzData(String zzData) throws Exception {
		if(zzDataPath == null) {
			throw new Exception("zzDataPath path not configured, check layout definition.");
		}
		StandardItem item = findItem(zzDataPath);
		if(item != null) {
			item.setValue(zzData);
		}
	}

	public StandardMessage() {
		this.name = "StandardRoot"; // XML 일 경우 Root name으로 사용 ?
		this.type = 1;
	}
	
	public int getReadPosition() {
		return readPosition;
	}
	public void setReadPosition(int readPosition) {
		this.readPosition = readPosition;
	}
	
	public StandardItem findItem(String path) {
		return findItem(path, '.', true, false);
	}
	
	private String[] split(String source, char spliter) {
    	List<String> list = new ArrayList<String>();
    	CharSequence chars = source;
    	int start = 0, end = 0;
    	for(int i=0; i<chars.length(); i++) {
    		if(chars.charAt(i) == spliter) {
    			end = i;
    			list.add(chars.subSequence(start, end).toString());
    			start = end+1;
    			i = i+1;
    		}
    	}
    	
    	if(end == 0) {
    		list.add(chars.toString());
    	}
    	else {
    		list.add(chars.subSequence(start, chars.length()).toString());
    	}
    	
    	return (String[])list.toArray(new String[0]);
    }
	
	public StandardItem findItem(String path, char pathSeparator, boolean zeroBase, boolean createNew) {
		String[] paths = split(path, pathSeparator);
		StandardItem item = null;

		LinkedHashMap<String , StandardItem> childItems = this.getChilds();
		for(int i=0; i< paths.length; i++) {
			String itemName = paths[i];
			int arrayIndex = StandardMessageUtil.getArrayIndex(itemName);
			
			if(arrayIndex >= 0) {
				itemName = StandardMessageUtil.getArrayName(itemName);
				// if XML, use 1 base index 
				if( !zeroBase ) {
					arrayIndex = arrayIndex -1;
				}
			}
			try {
				item = childItems.get(itemName);				
			}
			catch(Exception ex) {
				logger.error("get Child error : this=" + itemName, ex);		
				return null;
			}
			if(i< paths.length-1) {
				if(item != null && item.getType() >StandardType.FIELD) {
					if(arrayIndex >= 0) {
						childItems = item.getArrayChilds(arrayIndex, createNew);
					}
					else {
						childItems = item.getChilds();
					}
					if(childItems == null) {
						logger.warn("childItems not found : " + paths[i] +" / "+ path);
						return  null;
					}
				}
				else {
					logger.warn("group not found : " + paths[i] +" / "+ path);
					return null;
				}
			}			
		}
		if(item == null) {
			logger.warn("Item not found : " + path);
			return null;
		}
		else {
			return item;
		}
	}
	
	// group1.group2.item1
	public String findItemValue(String path) {
		StandardItem item = findItem(path);
		if(item == null) {
			return null;
		}
		else {
			return item.getValue();
		}
	}
	
	public void setData(String path, String value) {
		StandardItem item = findItem(path);
		if(item == null) {
			return;
		}
		else {
			item.setValue(value);
		}
	}
	
//	deprecated	
//	public String toFixedString() {
//		return toFixedString(true, this.bizDataCharset);
//	}
	
	public String toFixedString(String charset) {
		return toFixedString(true, charset);
	}
	
	public String toFixedString(boolean withBizData) {
		return toFixedString(withBizData, this.bizDataCharset);
	}
	
	public String toFixedString(boolean withBizData, String charset) {
		byte[] bytes = toByteArray(withBizData, charset);
		try {			
			logger.debug("Encoding : {}", charset );
			return new String(bytes, charset);
		} catch (UnsupportedEncodingException e) {
			logger.error("encoding fail, use default.", e);
			return new String(bytes);
		}
	}
	
	public String toJson(boolean withBizData) {
		return toJson(false, withBizData);
	}
	public String toPrettyJson(boolean withBizData) {
		return toJson(true, withBizData);
	}
	
	public byte[] toByteArray(String charset) {
		return toByteArray(true, charset);
	}
	
//	deprecated	
//	public byte[] toByteArray() {
//		return toByteArray(true, this.bizDataCharset);
//	}
	
	public String getDataString(String messageType, String charset) throws Exception {
		if (MessageType.JSON.equals(messageType)) {
			return toJson();
		} else if (MessageType.XML.equals(messageType)) {
			return toXML();
		} else if (MessageType.ASC.equals(messageType)) {
			return toFixedString(charset);
		} else if (MessageType.EBC.equals(messageType)) {
			logger.error("Unsupported message type - " + MessageType.EBC);
			throw new Exception("Unsupported msgType - " + messageType);
		}
		return null;
	}

	public byte[] getDataBytes(String messageType, String charset) throws Exception {
		if (MessageType.JSON.equals(messageType)) {
			return toJson().getBytes(charset);
		} else if (MessageType.XML.equals(messageType)) {
			return toXML().getBytes(charset);
		} else if (MessageType.ASC.equals(messageType)) {
			return toByteArray(charset);
		} else if (MessageType.EBC.equals(messageType)) {
			logger.error("Unsupported message type - " + MessageType.EBC);
			throw new Exception("Unsupported msgType - " + messageType);
		}
		return null;
	}
	public byte[] toByteArray(boolean withBizData, String charset) {
		//if(runFilter && flatFilter != null) { // runFilter 사용여부가 아니라 withBizData 여부여서 변경하고 Filter가 null이 아니면 로직수행으로 변경
		if( flatFilter != null) {
			flatFilter.doFilter(this);
		}
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		Iterator<String> keyIter = null;
		try {
			keyIter = childs.keySet().iterator();
			while(keyIter.hasNext()) {
				String key = keyIter.next();
				StandardItem item = childs.get(key);
				bos.write(item.toByteArray(withBizData, charset));
			}
			return bos.toByteArray();
		} catch(Exception e) {
			logger.error("toByteArray failed", e);
			return new byte[0];
		}
		finally {
			if(bos != null)
				try {
					bos.close();
				} catch (IOException e) {
					;
				}
		}
	}
	
	public int getBytesDataLength() {
		return getBytesDataLength(this.bizDataCharset);
	}
	
	public int getBytesDataLength(String charset) {
		Iterator<String> keyIter = null;
		int totalSize = 0;
		try {
			keyIter = childs.keySet().iterator();
			while(keyIter.hasNext()) {
				String key = keyIter.next();
				StandardItem item = childs.get(key);
				totalSize +=item.getBytesDataLength(charset);
			}
			return totalSize;
		} catch(Exception e) {
			logger.error("getBytesDataLength failed", e);
			return totalSize;
		}
		finally {			
		}
	}

	/**
	 * path로 하위에 있는 byte 길이 계산
	 * 
	 * @param path
	 * @return
	 */
	public int getBytesDataLengthForPath(String path) {
		Iterator<String> keyIter = null;
		int totalSize = 0;
		try {
			StandardItem find = findItem(path);
			keyIter = find.childs.keySet().iterator();
			while (keyIter.hasNext()) {
				String key = keyIter.next();
				StandardItem item = find.childs.get(key);
				totalSize += item.getBytesDataLength(this.bizDataCharset);
			}
			return totalSize;
		} catch (Exception e) {
			logger.error("getBytesDataLength failed", e);
			return totalSize;
		} finally {
		}
	}
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(getLevelTreeIndent()).append("StandardMessage [name=" + name + ", level=" + level 
				+ ", type=" + type 
//				+", size=" + size + ", fieldType=" + fieldType +", length=" + length + ", dataType=" + dataType
//				+ ", refPath=" + refPath + ", refValue=" + refValue
//				+ ", value=" + value 
				+ ", desc=" + desc + "]");
		if(bizDataPath != null) sb.append(", bizDataPath=" + bizDataPath);
		if(llDataPath != null) sb.append(", llDataPath=" + llDataPath);
		if(zzDataPath != null) sb.append(", zzDataPath=" + zzDataPath);
		
		
		Iterator<String> keyIter = childs.keySet().iterator();
		while(keyIter.hasNext()) {
			String key = keyIter.next();
			StandardItem item = childs.get(key);
			sb.append("\n").append(item.toString());
		}
		return sb.toString();
	}
	
	@Override
	public Object clone() throws CloneNotSupportedException {
		StandardMessage cloned = (StandardMessage)super.clone();
    	if(flatFilter != null) cloned.setFlatFilter(flatFilter);    	
    	return cloned;
    }
}