package com.eactive.eai.transformer.message;

import java.io.StringReader;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Stack;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.dom4j.Namespace;
import org.dom4j.QName;

import com.eactive.eai.transformer.SystemKeys;
import com.eactive.eai.transformer.layout.Converter;
import com.eactive.eai.transformer.layout.InvalidNodeException;
import com.eactive.eai.transformer.layout.Item;
import com.eactive.eai.transformer.layout.Layout;
import com.eactive.eai.transformer.layout.Types;
import com.eactive.eai.transformer.util.Constants;
import com.eactive.eai.transformer.util.Logger;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;


public class XMLMessage extends Message {
	private static final long serialVersionUID = -8580435953360297407L;

	protected static Logger logger = Logger.getLogger("transformer");

    public static final  String NAME = "XML";
    public static final String MESSAGE_NAME = "XMLMessage";
    public static final String NAMESPACE_PREFIX = "xmlns:";    
    public static final String REF_TYPE_ELEMENT = "ELEMENT";
    public static final String REF_TYPE_ATTRIBUTE = "ATTRIBUTE";
    public static final String REF_TYPE_TEXT = "TEXT";
    public static final String REF_TYPE_CDATA = "CDATA";
    public static final boolean INDENT = true;
    
	public static final String ELEMENT_CONTENT_LAYOUT_NAME = "ELEMENT_CONTENT";

    public static final Pattern pattern = Pattern.compile("\\[([0-9]+)\\]");
    protected org.dom4j.Element root;
    protected boolean bStaled = false;
    
	static int cacheSize = 0;
	static int cacheIdle = 0;	
	
	static Cache cache = null;    
    
    static {
    	cacheSize = Integer.parseInt(System.getProperty(SystemKeys.TRANSFROMER_XML_CACHE_SIZE, SystemKeys.XML_CACHE_SIZE_DEFAULT));
        cacheIdle = Integer.parseInt(System.getProperty(SystemKeys.TRANSFROMER_XML_CACHE_IDLE_SEC, SystemKeys.XML_CACHE_IDLE_DEFAULT));        
        initCache();
    }
    
    public static void initCache() {
    	try {
            CacheManager manager = CacheManager.create();
            logger.warn(">> XMLMessage initCache");
        	logger.warn(String.format("%s = %s", SystemKeys.TRANSFROMER_XML_CACHE_SIZE, cacheSize));
            logger.warn(String.format("%s = %s", SystemKeys.TRANSFROMER_XML_CACHE_IDLE_SEC, cacheIdle));
            cache = new Cache("xmlMessage", cacheSize, false, false, 0, cacheIdle);
            manager.addCache(cache);
        } catch(Exception e) {
            logger.error("XMLMessage cache create error",e);
        }
    }
    
    public XMLMessage() {
        super();    	
    }

    public void createObject() {
    	root = null;
    }

    /**
     * XML String 객체를 반환함
     */
    public Object getData() {
    	if (bStaled || root == null) {
            try {
            	this.root = makeXmlLogString(false);
            } catch(Exception e) {
                throw new InvalidDataTypeException(e.getMessage(),"XMLMessage.getData() method", e);   
            }
            bStaled = false;
        }
        return root.asXML();
    }

    public org.dom4j.Element getDataElement() {
        return root;
    }
   
    protected void setValue(org.dom4j.Element element) {            
    	/**
    	 * occRef 설정을 위해
    	 */
        HashMap<String, Item> validationTargets = new HashMap<String, Item>();    
        doSetValue(element, validationTargets);
        
        // validation
        if(SystemKeys.isValidationEnabled()) {
	        Iterator<String> iter = validationTargets.keySet().iterator();
	        while(iter.hasNext()) {
	            String itemPath = (String) iter.next();
	            Item item = (Item) validationTargets.get(itemPath);
	
	            int occCount = getInt(itemPath);

	            // 반복횟수참조정보 필드 설정에 따른 Validation
	            if (occCount < item.getOccMin() || 
	               (!"*".equals(item.getOccType()) && !"+".equals(item.getOccType()) && occCount > item.getOccMax())) 
	            	throw new InvalidAccessException("반복횟수가 OccMin~OccMax 의 범위를 벗어납니다. , " + item.getName(), item.getItemPath());
	            if (occCount != item.arraySize())
	            	throw new InvalidAccessException("반복횟수가 실제 데이타의 반복횟수와 틀립니다. , " + item.getName(), item.getItemPath());            
	        }
        } // validation
    }

    /**
     * occRef 설정을 위해
     */
    protected void doSetValue(org.dom4j.Element element, HashMap<String, Item> validationTargets) {
        
        Iterator<org.dom4j.Element> i = element.elementIterator();
        String childPath = null;
        while (i.hasNext()) {
            org.dom4j.Element child = (org.dom4j.Element) i.next();
            
            Item item = null;
            QName qn = new QName(child.getName(),Namespace.NO_NAMESPACE);
            child.setQName(qn);
            try {       	
            	childPath = denomalizeXPath(child.getUniquePath());
                item = getOrCreateItem(childPath);
                if (item==null)
                	throw new InvalidAccessException("Item 이 존재하지 않습니다.", childPath);

                // validation target
                if(SystemKeys.isValidationEnabled() && StringUtils.isNotEmpty(item.getOccRef())) {
                    validationTargets.put(item.getParent().getItemPath() + Types.FIELD_SEPARATOR + item.getOccRef(), item);
                }
            } catch(InvalidNodeException e) {
            	continue;
            } catch(Exception e) {
              throw new InvalidAccessException(e.getMessage(), 
            	childPath, 
                this,
        		e);
            }
            
            // Group의 Attribute 확인
            if (item.isGroup()) {
                int attrCnt = child.attributeCount();
                String groupPath = childPath + Types.FIELD_SEPARATOR;
                try {
                    for (int ai=0; ai<attrCnt; ai++) {                        
                       try {
                    	setString(groupPath+child.attribute(ai).getName()+"[0]" , 
                        (child.attribute(ai).getData() == null)? "" : (String)child.attribute(ai).getData());
                       }
	               		catch(Exception e) {
	               			continue;
	               		}
                    }                    
                } catch(Exception e) {
                    throw new InvalidAccessException(e.getMessage(), childPath, (Message) item.getRoot(), e);
                }
                
                // PPCB Group has value
                Object data = child.getData();
                try {
	            	if (data instanceof String){
	            		setString(childPath, (String) data);
	            	}else if (data instanceof byte[]){
	            		setString(childPath, new String((byte[]) data));
	            	}
	            	
					// XML에서 Attribute 가 있는 Element의 Content를
					// layout에서 특정 이름(ELEMENT_CONTENT)의 child Field로 정의할경우
					if (attrCnt > 0 && data != null) {
						String contentPath = groupPath + ELEMENT_CONTENT_LAYOUT_NAME + "[0]";
						Item contentItem = getItem(contentPath);
						if (contentItem != null) {
							if (data instanceof String) {
								setString(contentPath, (String) data);
							} else if (data instanceof byte[]) {
								setString(contentPath, new String((byte[]) data));
							}
						}
					}
            	}
            	catch(Exception InvalidNodeException) {
            		continue;
        		}
            }
            else if (item.isField()) {
            	Object data = child.getData();
            	if (data instanceof String
            			&& item.getDataTypeId() == Types.TYPE_BIGDECIMAL
            			&& SystemKeys.isValidationEnabled()
            			&& !Converter.isValid((String) data, item.getLength(), item.getDecimalPointLength())) {
            		throw new InvalidAccessException(
            			String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d",
            				data, item.getLength(), item.getDecimalPointLength()),
            			childPath);
            	}
            	try {
	            	if (data instanceof String){
	            		setString(childPath, (String) data);
	            	}else if (data instanceof byte[]){
	            		setString(childPath, new String((byte[]) data));
	            	}
            	}
        		catch(Exception InvalidNodeException) {
        			continue;
        		}
            }
            doSetValue(child, validationTargets);
        }
    }
    /**
     * org.w3c.dom.Document 인스턴스 또는 XML String, InputStream으로 설정 할 수 있음.
     */
    protected void doSetData(Object data) {
        try {
            String xmlData = null;
            if (data instanceof String) {                
            	xmlData = (String) data;
            } else {
                if (data == null) {
                    throw new InvalidDataTypeException("Not able to generate XML messages with data. Input value is null.", data);
                } else {
                    throw new InvalidDataTypeException("Not able to generate XML messages with data. data type = " + data.getClass().getName(), data);
                }
            }
            // use DOM
//            org.dom4j.io.DOMReader xmlReader = new org.dom4j.io.DOMReader();
//            org.dom4j.Document document = xmlReader.read((Document) this.data);
            // use SAX
            org.dom4j.io.SAXReader saxReader = new org.dom4j.io.SAXReader();
            org.dom4j.Document document = saxReader.read(new StringReader(xmlData));
            //-------------------------------------------------
            // if necessary to DO THIS
//          this.data = new DOMWriter().write(document);
          //-------------------------------------------------
            setValue(document.getRootElement());
        } catch (Exception e) {
            throw new MessageSetDataException(e.getMessage(), this, data, e);
        }
    }

    protected void composeData(Item itemInfo, Object value) throws InvalidAccessException {
    	if (itemInfo.getIndex() < 0)
            return;

        if (!bStaled) {
            bStaled = true;
        }
    }

    @Override
    public Layout getLayout() {
        return layout;
    }
    
    protected String denomalizeXPath(String xpath) {
    	try {
            net.sf.ehcache.Element element = cache.get(xpath);
            
            if (element != null) {
                return (String) element.getValue();
            }         
        } catch(CacheException e) {
        	logger.error(e.getMessage(), e);
        }
    	StringBuffer sb = new StringBuffer();
        String[] parts = xpath.split("/");
        for(int i=1; i<parts.length; i++) {
        	if(i == 1) {
        		sb.append(parts[i]);
        	}
        	else {
        		if(parts[i].charAt(parts[i].length()-1) ==']') {
        			int sPos = parts[i].indexOf("[");
        			String idx = parts[i].substring(sPos+1, parts[i].length()-1);
        			sb.append(parts[i].substring(0,sPos))
        			.append("[").append(Integer.parseInt(idx)-1).append("]");
        		}
        		else {
        			sb.append(parts[i]).append("[0]");
        		}
        	}
        	
        	if(i  < parts.length-1) {
        		sb.append(Types.FIELD_SEPARATOR);
        	}
        }
        String itemPath = sb.toString();
        cache.put(new net.sf.ehcache.Element(xpath, itemPath));
        return itemPath;
    }
    
	public String toLogString() {
		return ((org.dom4j.Element) makeXmlLogString(true)).asXML();
	}
	
    private org.dom4j.Element makeXmlLogString(boolean isMask) {
        // 표준전문의 XML subset이 root가 없을 수 있으므로 제거 불가함.
//    	boolean withLayoutName = false;
    	
    	org.dom4j.Document document = org.dom4j.DocumentHelper.createDocument();
        org.dom4j.Element rootElement = document.addElement(getName());
        
        Stack <org.dom4j.Element>stack = new Stack<org.dom4j.Element>();
        
        /**
         * Namespance :   Path에 Namespace를 입력
         */
        String paths = getPath();
        if(paths != null) {
            paths = paths.trim();
            String[] path = paths.split(" ");
            for(int i=0; i<path.length; i++) {
                if(path[i] != null && path[i].length() > 0) {
                    if(path[i].startsWith(NAMESPACE_PREFIX)) {
                        String prefix = path[i].substring(path[i].indexOf(NAMESPACE_PREFIX)+6, path[i].indexOf("=\""));
                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);                    
                        rootElement.addNamespace(prefix,nspath);
                    }
                    else if(path[i].indexOf("=\"") > -1) {
                        String prefix = path[i].substring(0,path[i].indexOf("=\""));
                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);                    
                        rootElement.addAttribute(prefix,nspath);
                    }                    
                }
            }
        }
        
        org.dom4j.Element parent = rootElement;
        org.dom4j.Element current = null;
        stack.push(rootElement);
        
		int prevDepth = 1;
        for (Iterator iter = iterator(); iter.hasNext();) {
			Item item = (Item) iter.next();
			 
			//index == -1 인 경우는 무시한다
			if (item == null || item.getIndex() == -1) {
				continue;
			}
			
			/**
			 * depth 가 낮아질 때(tree를 빠져 나올 때), parent를 재설정한다. 
			 */
			
			// 3중 그룹에 의해 XML 구조가 변경되는 로직 수정
			if (item.getDepth() < prevDepth) {
				for(int j=0; j<(prevDepth - item.getDepth()); j++) stack.pop();
			}
	        /**
	         * 이전 depth를 저장해 둔다.
	         */
			prevDepth = item.getDepth();	      
			
			parent = stack.peek();

			// "?" 속성인 경우 값이 스페이스로 채워져 있을 때 변경  
			// Number Type 속성의 값이 0 인 경우 Empty 처리 
            if( item.getOccMin() == 0 && item.getOccMax() == 1 ) {
            	if( item.getValue() == null ) continue;  
            	if( item.getValue() instanceof String && "".equals(((String)item.getValue()).trim()) ) continue;    
            	if( item.getValue() instanceof byte[] && "".equals(new String((byte[])item.getValue()).trim()) ) continue;
            	if( item.getValue() instanceof Number && ((Number)item.getValue()).floatValue() == 0.0f ) continue;          	
            }

			switch (item.getNodeTypeId()) {
			case NODE_ATTR:        
	            if (isMask && "Y".equals(item.getMask())) {
	                parent.addAttribute(item.getName(), maskString2(item.getValue().toString(), item.getMaskLength(), item.getMaskOffset()));                
	            } else {
	                if (item.getValue() == null) {
	                    parent.addAttribute(item.getName(), "");
	                }
	                else {
	                    parent.addAttribute(item.getName(), item.getValue().toString());
	                }
	            }
	            break;
	            
			case NODE_FIELD:
				// XML에서 Attribute 가 있는 Element의 Content를
				// layout에서 특정 이름(ELEMENT_CONTENT)의 child Field로 정의할경우
				if (StringUtils.equals(item.getName(), ELEMENT_CONTENT_LAYOUT_NAME)) {
					// parent content부에 셋팅한다.
					if (item.getValue() == null) {
						parent.addText("");
					} else {
						if (isMask && "Y".equals(item.getMask())) {
							parent.addText(maskString2(item.getValue().toString(), item.getMaskLength()));
						} else {
							Object value = item.getValue();
							if (value instanceof String) {
								parent.addText((String) value);
							} else if (value instanceof byte[]) {
								parent.addText(new String((byte[]) value));
							} else if (value instanceof BigDecimal) {
								parent.addText(((BigDecimal) value).toPlainString());
							} else if (value instanceof Integer) {
								parent.addText(String.valueOf(value));
							}
						}
					}

					continue;
				}
				
	            current = parent.addElement(item.getName());
		        if(paths != null ) {
		            paths = paths.trim();
		            String[] path = paths.split(" ");
		            for(int i=0; i<path.length; i++) {
		                if(path[i] != null && path[i].length() > 0) {
		                    if(path[i].startsWith(NAMESPACE_PREFIX)) {
		                        String prefix = path[i].substring(path[i].indexOf(NAMESPACE_PREFIX)+6, path[i].indexOf("=\""));
		                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);
		                        if (!"xsi".equals(prefix)){
			                        Namespace ns = new Namespace(prefix,nspath);
			                        QName qn = new QName(item.getName(),ns);
			                        current.setQName(qn);
		                        }
		                    }
		                }
		            }
		        }            
	            
	            /**
	             * Null인 경우 ""로 데이터를 Set하도록 한다
	             */
	            if(item.getValue() == null) {
	                current.addText("");
	            } else {
		            if (isMask && "Y".equals(item.getMask())) {
		                current.addText(maskString2(item.getValue().toString(), item.getMaskLength()));
		            } else {
		            	Object value = item.getValue();
		            	if (value instanceof String){
		            		current.addText((String)value);
		            	} else if (value instanceof byte[]){
		            		current.addText(new String((byte[])value));
		            	} else if ( value instanceof BigDecimal) {
		            		current.addText(((BigDecimal) value).toPlainString());
		            	} else if ( value instanceof Integer) {
		            		current.addText(String.valueOf(value));
		            	}
		            }
	            }
	            break;
	            
			case NODE_GROUP:
				// GROUP has Value, in some case
				current = parent.addElement(item.getName());
		        String itemPaths = item.getPath();
				if(itemPaths != null ) {
					itemPaths = itemPaths.trim();
		            String[] path = itemPaths.split(" ");
		            for(int i=0; i<path.length; i++) {
		                if(path[i] != null && path[i].length() > 0) {
		                    if(path[i].startsWith("xmlns:")) {
		                        String prefix = path[i].substring(path[i].indexOf("xmlns:")+6, path[i].indexOf("=\""));
		                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);
		                        if (!"xsi".equals(prefix)){
			                        Namespace ns = new Namespace(prefix,nspath);
			                        QName qn = new QName(item.getName(),ns);
			                        current.setQName(qn);
		                        }
		                    }
		                    else if(path[i].indexOf("=\"") > -1) {
		                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);                    
		                        Namespace ns = new Namespace("",nspath);
		                        QName qn = new QName(item.getName(),ns);
		                        current.setQName(qn);
		                    }
		                }
		            }
		        }            
	            
	            /**
	             * - Null인 경우 ""로 데이터를 Set하도록 한다.
	             */
	            if(item.getValue() == null) {
	                current.addText("");
	            } else {
		            if (isMask && "Y".equals(item.getMask())) {
		                current.addText(maskString2(item.getValue().toString(), item.getMaskLength()));
		            } else {
		            	Object value = item.getValue();
		            	if (value instanceof String){
		            		current.addText((String)value);
		            	} else if (value instanceof byte[]){
		            		current.addText(new String((byte[])value));
		            	} else if ( value instanceof BigDecimal) {
		            		current.addText(((BigDecimal) value).toPlainString());
		            	} else if ( value instanceof Integer) {
		            		current.addText(String.valueOf(value));
		            	}
		            }
	            }	            
				// 그룹의 Child가 있는 경우에만
				if(item.getChildCount() > 0){
					stack.push(current);
				}
	            break;
			case NODE_MESSAGE:
	            current = parent.addElement(item.getName());
		        if(paths != null) {
		            paths = paths.trim();
		            String[] path = paths.split(" ");
		            for(int i=0; i<path.length; i++) {
		                if(path[i] != null && path[i].length() > 0) {
		                    if(path[i].startsWith("xmlns:")) {
		                        String prefix = path[i].substring(path[i].indexOf("xmlns:")+6, path[i].indexOf("=\""));
		                        String nspath = path[i].substring(path[i].indexOf("=\"")+2, path[i].length()-1);
		                        current.addNamespace(prefix, nspath);
		                        if (!"xsi".equals(prefix)){
			                        Namespace ns = new Namespace(prefix,nspath);
			                        QName qn = new QName(item.getName(),ns);
			                        current.setQName(qn);
		                        }

		                    }
		                }
		            }
		        }	            
	            stack.push(current);				
				break;
			default : 
				break;
	        } // end switch  
		}

        return rootElement;
    }   
    
    private String getHeader() {
    	return getName() + " (" + MESSAGE_NAME + ")";
    }
    
    @Override
    public String toString() {
    	String xmlString = null;
        try {
        	xmlString = (String)getData();
        } catch (Exception e) {
            logger.error( MESSAGE_NAME + " : " + e.getMessage(), e);
        }

        StringBuilder sb = new StringBuilder();
        sb.append(getHeader());
        sb.append(super.toString());
        if (xmlString != null) {
            sb.append("\n" + Constants.TAB + " | data = \n" + xmlString + "");
        }
        sb.append("\n" + Constants.TAB + " +--------------------------------------------------------------------------------------------------+");

        return sb.toString();
    }
}
