package com.cardinal.core.util;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import java.io.ByteArrayInputStream;
import org.w3c.dom.Node;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.w3c.dom.NodeList;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.apache.xml.serialize.LineSeparator;
import java.io.IOException;
import java.io.StringWriter;
import CORE.TShipToCustomer;

/**
 *
 * @author  CCourtney
 * @version 
 */
public class COREXMLHelper
{
    
    private final DocumentBuilderFactory documentBuilderFactory_;
    private final DocumentBuilder documentBuilder_;
    private final DOMImplementation domImplementation_;

    /**
     * Private initializer used by static factory to initialize singleton
     */
    public COREXMLHelper()
    throws ParserConfigurationException
    {
        System.setProperty("org.apache.xerces.xni.parser.XMLParserConfiguration",
                           "org.apache.xerces.parsers.XMLGrammarCachingConfiguration");
        documentBuilderFactory_ = DocumentBuilderFactory.newInstance();
        documentBuilder_ = documentBuilderFactory_.newDocumentBuilder();        
        domImplementation_ = documentBuilder_.getDOMImplementation();        
    }
        
    /**
     * Prases a byte array into a DOM document.
     *
     * @param data Byte array to be parsed
     * @return DOM Document object containing parsed data
     * @throws IOExcpetion Thrown when problems occure reading the byte array
     * @throws SAXException Thrown when byte array does not contain valid XML
     */
    public Document parseDocument(byte[] data)
    throws SAXException, IOException
    {        
        Document document = documentBuilder_.parse(new ByteArrayInputStream(data));        
        return document;
    }
    
    /**
     * Creates a new document and imports the src_element as the root node.
     *
     * @param src_element Element object which is to be copied to the root node of the new document.
     * @return DOM Document object with a copy of src_element as the root node
     */
    public Document newDocumentFromElement(Element src_element)
    {
        String rootNodeTagName = src_element.getTagName();

        // Create new document
        DocumentType documentType = domImplementation_.createDocumentType(rootNodeTagName,
                                                                          "", 
                                                                          "http://dtdoft.cardinal.net/dtds/CTF-1.0.dtd");
        Document document = domImplementation_.createDocument("",
                                                              rootNodeTagName,
                                                              documentType);

        // Import transmisison node (we can cast to element since we've already made sure this was an element)
        // and set transaction/transmission ids and set the datetime
        Element dst_element = (Element)document.importNode(src_element, true);

        document.replaceChild(dst_element, document.getDocumentElement());
        
        return document;
    }
    
    /**
     * Serialize a DOM document into a string. 
     *
     * @param document Document to be serialzied
     * @return XML String representation of document.
     */
    public String serializeDocument(Document document)
    throws IOException
    {
        String results;
        StringWriter out = new StringWriter();
        OutputFormat format = new OutputFormat(document);
        format.setLineSeparator(LineSeparator.Unix);
        format.setLineWidth(0);
        format.setPreserveSpace(true);
        XMLSerializer serializer = new XMLSerializer(out, format);
        serializer.serialize(document);
        results = out.toString();
        out.close();
        return results;
    }
    
    /**
     * Adds a simple unique tag / text node to an existing DOM tree.  This function will
     * replace existing node elements if the exist with the same name.  In addition if 
     * there are multiple of that tags already it will elemenate the duplicates.
     *
     * @param document Document object of the existing DOM tree
     * @param parentElement Element to which these children will be added
     * @param elementName Tag name of the element to add
     * @param elementValue Value to place in the new element
     * @param followingTags Array containing the list of all tags that follow
     *                          this tag in the DTD.  If the existing document tree
     *                          is well formed then this only needs to contain
     *                          tag names up to the next required tag.
     * @param replaceOnlyEmpty If true if only empty elements will be replaced 
     *                           elements containing values will be left as is.
     *                           If false the element is found it will be
     *                           replaced always.
     */
    public void addUniqueElement(Document document, Element parentElement, String elementName, String elementValue, String[] followingTags, boolean replaceOnlyEmpty)
    {
        boolean elementAppended = false;

        // Create new element
        Element newElement = document.createElement(elementName);
        Node txtNode = document.createTextNode(elementValue);
        newElement.appendChild(txtNode);

        // Check if element already exists if so replace the first instance
        // and remove any duplicates
        NodeList currentNodes = parentElement.getElementsByTagName(elementName);
        if (currentNodes.getLength() > 0) 
        {
            Element firstInstance = (Element)currentNodes.item(0);
            Node firstInstanceTextNode = firstInstance.getFirstChild();
                            
            if (replaceOnlyEmpty && 
                firstInstanceTextNode != null &&
                firstInstanceTextNode.getNodeValue() != null &&
                firstInstanceTextNode.getNodeValue().trim().length() > 0)
            {
                // Found an existing element that already had a value
                // since replaceOnlyEmpty is true then we should not
                // modify anything.
                elementAppended = true;
            }
            else 
            {
                // Either replaceOnlyEmpty is false or a empty/null value 
                // was found for the first instance so go ahead and replace
                parentElement.replaceChild(newElement, currentNodes.item(0));
                for (int delNodeIndex = 1; delNodeIndex < currentNodes.getLength(); delNodeIndex++)
                {
                    parentElement.removeChild(currentNodes.item(delNodeIndex));
                }
                elementAppended = true;
            }
        }

        // We didn't have an existing tag with that name so we will
        // loop through children of parentChild in order checking
        // each against the following list.  As soon as you hit
        // an hit a match insert before that element.
        else if (followingTags != null)
        {
            Node currentChildNode = parentElement.getFirstChild();
            while (currentChildNode != null && !elementAppended)
            {
                if (currentChildNode.getNodeType() == Node.ELEMENT_NODE)
                {
                    String currentTag = currentChildNode.getNodeName();
                    for (int followingTagIndex = 0; 
                         followingTagIndex < followingTags.length && !elementAppended; 
                         followingTagIndex++)
                    {
                        if (currentTag.equals(followingTags[followingTagIndex])
                            || followingTags[followingTagIndex].equals("*")) 
                        {
                            parentElement.insertBefore(newElement, currentChildNode);
                            elementAppended = true;
                        }
                    }
                }
                currentChildNode = currentChildNode.getNextSibling();
            }
        }

        // if no matches lets append to the end
        if (!elementAppended)
        {
            parentElement.appendChild(newElement);
        }
    }    
    
    /**
     * Returns the first child element matching the given tag name.
     *
     * @param element Element to search for child element
     * @param childTagName Tag to extract from parent
     * @return First child node matching the given tag name.
     */
    public Node getFirstChildElementByName(Element element, String childTagName)
    {
        Node retValue = null;
        
        NodeList currentNodes = element.getElementsByTagName(childTagName);
        if (currentNodes.getLength() > 0) 
        {
            retValue = currentNodes.item(0);
        }

        return retValue;
    }
    
    /**
     * Returns the node value as a string from a given child entity of a given element.
     *
     * @param parentElement Element which has the child to look for the given tag
     * @param tagName Tag name of the child element to get the value
     */
    public String getStringValueOfChild(Element parentElement, String tagName)
    {
        String retValue = null;
        NodeList childList = parentElement.getElementsByTagName(tagName);

        for (int i=0; i < childList.getLength(); i++)
        {
            Node valueNode = childList.item(i);
            Node valueNodeChild = valueNode.getFirstChild();
            if (valueNodeChild != null)
            {
                retValue = valueNodeChild.getNodeValue();
            }
        }    

        return retValue;
    }    
    
    /**
     * Extracts the ship to custmer and divsion from the given element children
     */
    public TShipToCustomer extractShipToCustomer(Element element)
    {
        TShipToCustomer customer = new TShipToCustomer();
        customer.customerNumber = getStringValueOfChild(element, "shiptocust");
        customer.divisionNumber = getStringValueOfChild(element, "shiptodiv");
        return customer;
    }
}