package org.adelbs.iso8583.helper;

import java.awt.Component;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Enumeration;
import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.tree.DefaultMutableTreeNode;

import org.adelbs.iso8583.constants.DelimiterEnum;
import org.adelbs.iso8583.constants.EncodingEnum;
import org.adelbs.iso8583.constants.TypeEnum;
import org.adelbs.iso8583.constants.TypeLengthEnum;
import org.adelbs.iso8583.exception.OutOfBoundsException;
import org.adelbs.iso8583.gui.ISOConfigGuiConverter;
import org.adelbs.iso8583.gui.PnlMain;
import org.adelbs.iso8583.gui.xmlEditor.XmlTextPane;
import org.adelbs.iso8583.protocol.ISO8583Delimiter;
import org.adelbs.iso8583.util.ISOUtils;
import org.adelbs.iso8583.vo.FieldVO;
import org.adelbs.iso8583.vo.GenericIsoVO;
import org.adelbs.iso8583.vo.ISOConfigVO;
import org.adelbs.iso8583.vo.MessageVO;
import org.adelbs.iso8583.xml.ISOConfigMarshaller;
import org.adelbs.iso8583.xml.ISOConfigMarshallerException;
import org.adelbs.iso8583.xml.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import groovy.util.Eval;

public class Iso8583Config {
	
	private static final String XML_FIELD_NODENAME = "field";
	private DefaultMutableTreeNode configTreeNode;
	private XmlTextPane xmlText = new XmlTextPane();
	
    private DelimiterEnum isoDelimiter;
    private EncodingEnum headerEncoding;
    private Integer headerSize;
	
	//Arquivo de configuracao carregado
	private String xmlFilePath = null;
	
	public Iso8583Config(String fileName) {
		this();
		openFile(null, fileName);
		parseXmlToConfig(null);
	}
	
	public Iso8583Config() {
        isoDelimiter = DelimiterEnum.getDelimiter("");
        headerEncoding = EncodingEnum.getEncoding("");
        headerSize = 0;
		configTreeNode = new DefaultMutableTreeNode("ISO8583");
	}
	
	public DefaultMutableTreeNode getConfigTreeNode() {
		return configTreeNode;
	}
	
	public XmlTextPane getXmlText() {
		return xmlText;
	}
	
	public DefaultMutableTreeNode addType() {
		MessageVO parseVO = new MessageVO("0000", EncodingEnum.UTF8);
		DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(parseVO);
		configTreeNode.add(newNode);
		return newNode;
	}

	public DefaultMutableTreeNode addField(PnlMain pnlMain, Object node) {
		DefaultMutableTreeNode newNode = null;
		
		if (isAMessageNode(node) || isAFieldNode(node)) {
			newNode = new DefaultMutableTreeNode(new FieldVO(pnlMain, "NewField", "", 2, TypeEnum.ALPHANUMERIC, TypeLengthEnum.FIXED, 1, EncodingEnum.UTF8, ""));
			((DefaultMutableTreeNode) node).add(newNode);
		}
		
		return newNode;
	}

	/**
	 * Check whether this node object is or isn't a {@link FieldVO} element;
	 * @param node {@link GenericIsoVO} object
	 * @return True if its a FieldVO, False otherwise.
	 */
	private boolean isAFieldNode(final Object node) {
		return ((DefaultMutableTreeNode) node).getUserObject() instanceof FieldVO;
	}

	/**
	 * Check whether this node object is or isn't a {@link MessageVO} element;
	 * @param node {@link GenericIsoVO} object
	 * @return True if its a MessageVO, False otherwise.
	 */
	private boolean isAMessageNode(final Object node) {
		return ((DefaultMutableTreeNode) node).getUserObject() instanceof MessageVO;
	}
	
	@SuppressWarnings("unchecked")
	public void updateSumField(Object node) {
		Enumeration<DefaultMutableTreeNode> enu = ((DefaultMutableTreeNode) node).children();
		FieldVO fieldVo = (FieldVO) ((DefaultMutableTreeNode) node).getUserObject();
		
		String name = fieldVo.getName();
		int sum = 0;
		int count = 1;
		
		while (enu.hasMoreElements()) {
			fieldVo = (FieldVO) enu.nextElement().getUserObject();
			fieldVo.setName(name + count);
			
			sum += fieldVo.getLength();
			count++;
		}
		
		fieldVo = (FieldVO) ((DefaultMutableTreeNode) node).getUserObject();
		
		if (count > 1)
			fieldVo.setLength(sum);
	}
	
	public void parseConfigToXML() throws ISOConfigMarshallerException {
		final ISOConfigMarshaller xmlParser = ISOConfigMarshaller.creatMarshaller();
		final ISOConfigVO isoConfigVO = ISOConfigGuiConverter.revert(configTreeNode);
        isoConfigVO.setDelimiter(isoDelimiter);
        isoConfigVO.setHeaderEncoding(headerEncoding);
        isoConfigVO.setHeaderSize(headerSize);
		xmlText.setText(xmlParser.marshal(isoConfigVO));
	}
	
	//TODO: Could be substituted by a ISOConfigMarshaller.unmarshal method
	public void parseXmlToConfig(PnlMain pnlMain) {
		try {
			if (!xmlText.getText().trim().equals("")) {
				configTreeNode.removeAllChildren();
				
				Document document = XMLUtils.convertXMLToDOM(xmlText.getText());
				
				DefaultMutableTreeNode lastParseNode;
				
                setDelimiterEnum(DelimiterEnum.getDelimiter(document.getDocumentElement().getAttribute("delimiter")));
                setHeaderEncoding(EncodingEnum.getEncoding(document.getDocumentElement().getAttribute("headerEncoding")));
                
                try {
                    setHeaderSize(Integer.parseInt(document.getDocumentElement().getAttribute("headerSize")));
                }
                catch (Exception x) {
                    setHeaderSize(0);
                }
				
				NodeList nodeList = document.getDocumentElement().getChildNodes();
				Node node;
				
				for (int i = 0; i < nodeList.getLength(); i++) {
					node = nodeList.item(i);
					
					if ("message".equalsIgnoreCase(node.getNodeName())) {
						lastParseNode = addType();
						
						((MessageVO) lastParseNode.getUserObject()).setType(ISOUtils.getAttr(node, "type", "0000"));;
						((MessageVO) lastParseNode.getUserObject()).setBitmatEncoding(EncodingEnum.getEncoding(ISOUtils.getAttr(node, "bitmap-encoding", "")));
						
						addFieldsToTree(pnlMain, node, lastParseNode);
					}
				}
			}
		}
		catch (Exception x) {
			x.printStackTrace();
			JOptionPane.showMessageDialog(pnlMain, "Invalid XML! See the log file.\n\n" + x.getMessage());
		}
	}

	public MessageVO getMessageVOAtTree(String type) {
		MessageVO newMessageVO = null;
		final ISOConfigVO isoConfigVO = ISOConfigGuiConverter.revert(configTreeNode);
		final List<MessageVO> messages = isoConfigVO.getMessageList();
		for (MessageVO messageVO : messages) {
			if (messageVO.getType().equalsIgnoreCase(type)) {
                newMessageVO = messageVO.getInstanceCopy();
                
                newMessageVO.setHeaderEncoding(getHeaderEncoding());
                newMessageVO.setHeaderSize(getHeaderSize());   

				break;
			}
		}		
		return newMessageVO;
	}

	private void addFieldsToTree(PnlMain pnlMain, Node domNode, DefaultMutableTreeNode lastParseUINode) {
		NodeList fielNodedList = domNode.getChildNodes();
		for (int j = 0; j < fielNodedList.getLength(); j++) {
			domNode = fielNodedList.item(j);
			addFieldToTree(pnlMain, domNode, lastParseUINode, false);
		}
	}
	
	/**
	 * Add a single Message Field, and its children fields, to the panel tree. 
	 * Only tags {@link XML_FIELD_NODENAME} are considered message fields. All other tags will be disconsidered.
	 * 
	 * @param pnlMain parent panel element.
	 * @param domNode root DOM node with the XML structure of the field we are adding to the panel.
	 * @param lastParseUINode last added UI node element.
	 * @param isSubField indicates that this field is a inner field of a "<i>pre-created</i>" field, enabling nested fields.
	 */
	private void addFieldToTree(final PnlMain pnlMain, Node domNode, DefaultMutableTreeNode lastParseUINode, boolean isSubField){	
		if (XML_FIELD_NODENAME.equalsIgnoreCase(domNode.getNodeName())) {
			DefaultMutableTreeNode newUINode = addField(pnlMain, lastParseUINode);
			FieldVO fieldVo = (FieldVO) newUINode.getUserObject();
			populateField(domNode, fieldVo, newUINode, lastParseUINode, isSubField);
			
			NodeList subFieldNodeList = domNode.getChildNodes();
			if (subFieldNodeList.getLength() > 0) {
				for (int i = 0; i < subFieldNodeList.getLength(); i++) {
					addFieldToTree(pnlMain, subFieldNodeList.item(i), newUINode, true);
				}
				updateSumField(newUINode);
			}
		}
	}
	
	
	private void populateField(Node node, FieldVO fieldVo, DefaultMutableTreeNode newNode, DefaultMutableTreeNode lastParseNode, boolean isSubfield) {

		fieldVo.setName(ISOUtils.getAttr(node, "name", ""));
		
		if (isSubfield)
			fieldVo.setSubFieldName(ISOUtils.getAttr(node, "name", ""));

		fieldVo.setBitNum(Integer.parseInt(ISOUtils.getAttr(node, "bitnum", "0")));
		fieldVo.setType(TypeEnum.getType(ISOUtils.getAttr(node, "type", "")));
		fieldVo.setTypeLength(TypeLengthEnum.getTypeLength(ISOUtils.getAttr(node, "length-type", "")));
		fieldVo.setLength(Integer.parseInt(ISOUtils.getAttr(node, "length", "0")));
		fieldVo.setEncoding(EncodingEnum.getEncoding(ISOUtils.getAttr(node, "encoding", "")));
		fieldVo.setDynaCondition(ISOUtils.getAttr(node, "condition", ""));
	}
	
	public void openFile(Component mainFrame, String file) {
		
		this.xmlFilePath = file;
		
		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(file));
		    StringBuilder sb = new StringBuilder();
		    String line = br.readLine();

		    while (line != null) {
		        sb.append(line);
		        sb.append(System.lineSeparator());
		        line = br.readLine();
		    }
		    xmlText.setText(sb.toString());
		} 
		catch (Exception x) {
			this.xmlFilePath = null;
			x.printStackTrace();
			
			if (mainFrame != null)
				JOptionPane.showMessageDialog(mainFrame, "Unable to read the file. See the log.\n\n"+ x.getMessage());
		}
		finally {
			if (br != null) {
				try {
					br.close();
				}
				catch (Exception x) {
					x.printStackTrace();
				}
			}
		}
	}
	
	public boolean newFile(Component mainFrame) {
		
		boolean result = false;
		
		if (JOptionPane.showConfirmDialog(mainFrame, "Are you sure? All changes will be lost.", "New File", JOptionPane.YES_NO_OPTION) == 0) {
			xmlText.setText("<j8583-config></j8583-config>");
			result = true;
		}
		
		return result;
	}
	
	public void saveFile(Component mainFrame, String filePath) {
		
		BufferedWriter output = null;
		
		try {
			if (!filePath.equals("")) {
				this.xmlFilePath = filePath;
				File file = new File(filePath);
	
				if (!file.exists()) {
					file.createNewFile();
				}
				
	            output = new BufferedWriter(new FileWriter(file));
	            output.write(xmlText.getText());
	            
				JOptionPane.showMessageDialog(mainFrame, "File saved successfully!");
			}
			else {
				this.xmlFilePath = null;
			}
		} 
		catch (Exception x) {
			this.xmlFilePath = null;
			x.printStackTrace();
			JOptionPane.showMessageDialog(mainFrame, "Error saving the XML file. See the log.\n\n"+ x.getMessage());
		}
		finally {
			if ( output != null ) {
				try {
					output.close();
				}
				catch (Exception x) {
					x.printStackTrace();
				}
			}
		}
	}
	
	public String validateCondition(FieldVO fieldVO) {
		String resultMessage = "";
		
		try {
			String condition = "Object[] BIT = new Object[255];\n";
			condition = condition + fieldVO.getDynaCondition();
			Eval.me(condition);
			
			if (condition.indexOf("BIT[" + fieldVO.getBitNum() + "]") > -1)
				throw new Exception("You cannot look for the same bit value.");
		}
		catch (Exception x) {
			resultMessage = x.getMessage();
		}
		
		return resultMessage;
	}
	
	public String getXmlFilePath() {
		return xmlFilePath;
	}
	
	public DelimiterEnum getDelimiterEnum() {
		return isoDelimiter;
	}
    
    public EncodingEnum getHeaderEncoding() {
        return headerEncoding;
    }

    public Integer getHeaderSize() {
        return headerSize;
    }

	public void setDelimiterEnum(DelimiterEnum isoDelimiter) {
		this.isoDelimiter = isoDelimiter;
	}
    
    public void setHeaderEncoding(EncodingEnum headerEncoding) {
        this.headerEncoding = headerEncoding;
    }

    public void setHeaderSize(Integer headerSize) {
        this.headerSize = headerSize;
    }

	public ISO8583Delimiter getDelimiter() {
		return isoDelimiter.getDelimiter();
	}
	
	/**
	 * It returns the MessageVO from the loaded tree, according to what comes from the payload
	 */
	public MessageVO findMessageVOByPayload(byte[] payload) {
		MessageVO result = null;
		try {
			for (int i = 0; i < configTreeNode.getChildCount(); i++) {
                result = (MessageVO) ((DefaultMutableTreeNode) configTreeNode.getChildAt(i)).getUserObject();
                
                int messageTypeSize = (headerEncoding == EncodingEnum.BCD) ? 2 : 4;
                int calculatedHeaderSize = (headerEncoding == EncodingEnum.BCD) ? (headerSize / 2) : headerSize;
                String messageType = headerEncoding.convert(ISOUtils.subArray(payload, calculatedHeaderSize, (calculatedHeaderSize + messageTypeSize)));
                
				if (result.getType().equals(messageType))
					break;
				else
					result = null;
			}
		}
		catch (OutOfBoundsException e) {
			e.printStackTrace();
		}
		
		return result;
	}
}