/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.debug.internal.core;


import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.debug.core.DebugPlugin;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * An XML memento. Copied from workspace plug-in.
 */
public final class XMLMemento {
    private Document factory;

    private Element element;

    /**
     * Creates a <code>Document</code> from the <code>Reader</code>
     * and returns a memento on the first <code>Element</code> for reading
     * the document.
     * <p>
     * Same as calling createReadRoot(reader, null)
     * </p>
     *
     * @param reader the <code>Reader</code> used to create the memento's document
     * @return a memento on the first <code>Element</code> for reading the document
     * @throws Exception if IO problems, invalid format, or no element.
     */
    public static XMLMemento createReadRoot(Reader reader)
            throws Exception {
        return createReadRoot(reader, null);
    }

    /**
     * Creates a <code>Document</code> from the <code>Reader</code>
     * and returns a memento on the first <code>Element</code> for reading
     * the document.
     *
     * @param reader the <code>Reader</code> used to create the memento's document
     * @param baseDir the directory used to resolve relative file names
     * 		in the XML document. This directory must exist and include the
     * 		trailing separator. The directory format, including the separators,
     * 		must be valid for the platform. Can be <code>null</code> if not
     * 		needed.
     * @return a memento on the first <code>Element</code> for reading the document
     * @throws Exception if IO problems, invalid format, or no element.
     */
    public static XMLMemento createReadRoot(Reader reader, String baseDir)
            throws Exception {
        String errorMessage = null;
        Exception exception = null;

        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory
                    .newInstance();
            DocumentBuilder parser = factory.newDocumentBuilder();
            InputSource source = new InputSource(reader);
            if (baseDir != null) {
				source.setSystemId(baseDir);
			}
            Document document = parser.parse(source);
            NodeList list = document.getChildNodes();
            for (int i = 0; i < list.getLength(); i++) {
                Node node = list.item(i);
                if (node instanceof Element) {
					return new XMLMemento(document, (Element) node);
				}
            }
        } catch (ParserConfigurationException e) {
            exception = e;
           // errorMessage = WorkbenchMessages.XMLMemento_parserConfigError;
        } catch (IOException e) {
            exception = e;
           // errorMessage = WorkbenchMessages.XMLMemento_ioError;
        } catch (SAXException e) {
            exception = e;
           // errorMessage = WorkbenchMessages.XMLMemento_formatError;
        }

        String problemText = null;
        if (exception != null) {
			problemText = exception.getMessage();
		}
        if (problemText == null || problemText.length() == 0) {
			problemText = errorMessage != null ? errorMessage
                    :"ERROR"; //;WorkbenchMessages.XMLMemento_noElement; //$NON-NLS-1$
		}
        throw new Exception(problemText, exception);
    }

    /**
     * Returns a root memento for writing a document.
     *
     * @param type the element node type to create on the document
     * @return the root memento for writing a document
     */
    public static XMLMemento createWriteRoot(String type) {
        Document document;
        try {
            document = DocumentBuilderFactory.newInstance()
                    .newDocumentBuilder().newDocument();
            Element element = document.createElement(type);
            document.appendChild(element);
            return new XMLMemento(document, element);
        } catch (ParserConfigurationException e) {
//            throw new Error(e);
            throw new Error(e.getMessage());
        }
    }

    /**
     * Creates a memento for the specified document and element.
     * <p>
     * Clients should use <code>createReadRoot</code> and
     * <code>createWriteRoot</code> to create the initial
     * memento on a document.
     * </p>
     *
     * @param document the document for the memento
     * @param element the element node for the memento
     */
    public XMLMemento(Document document, Element element) {
        super();
        this.factory = document;
        this.element = element;
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public XMLMemento createChild(String type) {
        Element child = factory.createElement(type);
        element.appendChild(child);
        return new XMLMemento(factory, child);
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public XMLMemento createChild(String type, String id) {
        Element child = factory.createElement(type);
        child.setAttribute(IMementoConstants.TAG_ID, id == null ? "" : id); //$NON-NLS-1$
        element.appendChild(child);
        return new XMLMemento(factory, child);
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public XMLMemento copyChild(XMLMemento child) {
        Element childElement = child.element;
        Element newElement = (Element) factory.importNode(childElement, true);
        element.appendChild(newElement);
        return new XMLMemento(factory, newElement);
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public XMLMemento getChild(String type) {

        // Get the nodes.
        NodeList nodes = element.getChildNodes();
        int size = nodes.getLength();
        if (size == 0) {
			return null;
		}

        // Find the first node which is a child of this node.
        for (int nX = 0; nX < size; nX++) {
            Node node = nodes.item(nX);
            if (node instanceof Element) {
                Element element1 = (Element) node;
                if (element1.getNodeName().equals(type)) {
					return new XMLMemento(factory, element1);
				}
            }
        }

        // A child was not found.
        return null;
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public XMLMemento[] getChildren(String type) {

        // Get the nodes.
        NodeList nodes = element.getChildNodes();
        int size = nodes.getLength();
        if (size == 0) {
			return new XMLMemento[0];
		}

        // Extract each node with given type.
		ArrayList<Element> list = new ArrayList<>(size);
        for (int nX = 0; nX < size; nX++) {
            Node node = nodes.item(nX);
            if (node instanceof Element) {
                Element element1 = (Element) node;
                if (element1.getNodeName().equals(type)) {
					list.add(element1);
				}
            }
        }

        // Create a memento for each node.
        size = list.size();
        XMLMemento[] results = new XMLMemento[size];
        for (int x = 0; x < size; x++) {
            results[x] = new XMLMemento(factory, list.get(x));
        }
        return results;
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public Float getFloat(String key) {
        Attr attr = element.getAttributeNode(key);
        if (attr == null) {
			return null;
		}
        String strValue = attr.getValue();
        try {
            return Float.valueOf(strValue);
        } catch (NumberFormatException e) {
           DebugPlugin.logMessage("Memento problem - Invalid float for key: " //$NON-NLS-1$
                    + key + " value: " + strValue, e); //$NON-NLS-1$
            return null;
        }
    }

	public String getType() {
		return element.getNodeName();
	}

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public String getID() {
        return element.getAttribute(IMementoConstants.TAG_ID);
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public Integer getInteger(String key) {
        Attr attr = element.getAttributeNode(key);
        if (attr == null) {
			return null;
		}
        String strValue = attr.getValue();
        try {
            return Integer.valueOf(strValue);
        } catch (NumberFormatException e) {
            DebugPlugin
                    .logMessage("Memento problem - invalid integer for key: " + key //$NON-NLS-1$
                            + " value: " + strValue, e); //$NON-NLS-1$
            return null;
        }
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public String getString(String key) {
        Attr attr = element.getAttributeNode(key);
        if (attr == null) {
			return null;
		}
        return attr.getValue();
    }

	public Boolean getBoolean(String key) {
        Attr attr = element.getAttributeNode(key);
        if (attr == null) {
			return null;
		}
        return Boolean.valueOf(attr.getValue());
	}

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public String getTextData() {
        Text textNode = getTextNode();
        if (textNode != null) {
            return textNode.getData();
        }
        return null;
    }

	public String[] getAttributeKeys() {
		NamedNodeMap map = element.getAttributes();
		int size = map.getLength();
		String[] attributes = new String[size];
		for (int i = 0; i < size; i++) {
			Node node = map.item(i);
			attributes[i] = node.getNodeName();
		}
		return attributes;
	}

    /**
     * Returns the Text node of the memento. Each memento is allowed only
     * one Text node.
     *
     * @return the Text node of the memento, or <code>null</code> if
     * the memento has no Text node.
     */
    private Text getTextNode() {
        // Get the nodes.
        NodeList nodes = element.getChildNodes();
        int size = nodes.getLength();
        if (size == 0) {
			return null;
		}
        for (int nX = 0; nX < size; nX++) {
            Node node = nodes.item(nX);
            if (node instanceof Text) {
                return (Text) node;
            }
        }
        // a Text node was not found
        return null;
    }

    /**
     * Places the element's attributes into the document.
     * @param element1 the element
     * @param copyText true if the first text node should be copied
     */
    private void putElement(Element element1, boolean copyText) {
        NamedNodeMap nodeMap = element1.getAttributes();
        int size = nodeMap.getLength();
        for (int i = 0; i < size; i++) {
            Attr attr = (Attr) nodeMap.item(i);
            putString(attr.getName(), attr.getValue());
        }

        NodeList nodes = element1.getChildNodes();
        size = nodes.getLength();
        // Copy first text node (fixes bug 113659).
        // Note that text data will be added as the first child (see putTextData)
        boolean needToCopyText = copyText;
        for (int i = 0; i < size; i++) {
            Node node = nodes.item(i);
            if (node instanceof Element) {
                XMLMemento child = createChild(node.getNodeName());
                child.putElement((Element) node, true);
            } else if (node instanceof Text && needToCopyText) {
                putTextData(((Text) node).getData());
                needToCopyText = false;
            }
        }
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public void putFloat(String key, float f) {
        element.setAttribute(key, String.valueOf(f));
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public void putInteger(String key, int n) {
        element.setAttribute(key, String.valueOf(n));
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public void putMemento(XMLMemento memento) {
    	// Do not copy the element's top level text node (this would overwrite the existing text).
    	// Text nodes of children are copied.
        putElement(memento.element, false);
    }

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public void putString(String key, String value) {
        if (value == null) {
			return;
		}
        element.setAttribute(key, value);
    }

	/**
	 * @param key the attribute key
	 * @param value the boolean value for the key
	 * @since 3.4
	 */
	public void putBoolean(String key, boolean value) {
		element.setAttribute(key, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
	}

    /* (non-Javadoc)
     * Method declared in XMLMemento.
     */
    public void putTextData(String data) {
        Text textNode = getTextNode();
        if (textNode == null) {
            textNode = factory.createTextNode(data);
			// Always add the text node as the first child (fixes bug 93718)
			element.insertBefore(textNode, element.getFirstChild());
        } else {
            textNode.setData(data);
        }
    }

    /**
     * Saves this memento's document current values to the
     * specified writer.
     *
     * @param writer the writer used to save the memento's document
     * @throws IOException if there is a problem serializing the document to the stream.
     */
    public void save(Writer writer) throws IOException {
		try (DOMWriter out = new DOMWriter(writer)) {
        	out.print(element);
		}
	}

	/**
     * A simple XML writer.  Using this instead of the javax.xml.transform classes allows
     * compilation against JCL Foundation (bug 80053).
     */
    private static final class DOMWriter extends PrintWriter {

//    	private int tab;

    	/* constants */
    	private static final String XML_VERSION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$

    	/**
    	 * Creates a new DOM writer on the given output writer.
    	 *
    	 * @param output the output writer
    	 */
    	public DOMWriter(Writer output) {
    		super(output);
//    		tab = 0;
    		println(XML_VERSION);
    	}

    	/**
    	 * Prints the given element.
    	 *
    	 * @param element the element to print
    	 */
        public void print(Element element) {
        	// Ensure extra whitespace is not emitted next to a Text node,
        	// as that will result in a situation where the restored text data is not the
        	// same as the saved text data.
        	boolean hasChildren = element.hasChildNodes();
        	startTag(element, hasChildren);
        	if (hasChildren) {
//	        	tab++;
	        	boolean prevWasText = false;
	        	NodeList children = element.getChildNodes();
	    		for (int i = 0; i < children.getLength(); i++) {
	    			Node node = children.item(i);
	    			if (node instanceof Element) {
	    				if (!prevWasText) {
	    					println();
	    					printTabulation();
	    				}
	    				print((Element) children.item(i));
	    				prevWasText = false;
	    			}
	    			else if (node instanceof Text) {
	    				print(getEscaped(node.getNodeValue()));
	    				prevWasText = true;
	    			}
	    		}
//	    		tab--;
	    		if (!prevWasText) {
	    			println();
	    			printTabulation();
	    		}
	    		endTag(element);
        	}
    	}

    	private void printTabulation() {
        	// Indenting is disabled, as it can affect the result of getTextData().
        	// In 3.0, elements were separated by a newline but not indented.
    		// This causes getTextData() to return "\n" even if no text data had explicitly been set.
        	// The code here emulates that behaviour.

//    		for (int i = 0; i < tab; i++)
//    			super.print("\t"); //$NON-NLS-1$
    	}

    	private void startTag(Element element, boolean hasChildren) {
    		StringBuilder sb = new StringBuilder();
    		sb.append("<"); //$NON-NLS-1$
    		sb.append(element.getTagName());
    		NamedNodeMap attributes = element.getAttributes();
   			for (int i = 0;  i < attributes.getLength(); i++) {
   				Attr attribute = (Attr)attributes.item(i);
				sb.append(" "); //$NON-NLS-1$
				sb.append(attribute.getName());
				sb.append("=\""); //$NON-NLS-1$
				sb.append(getEscaped(String.valueOf(attribute.getValue())));
				sb.append("\""); //$NON-NLS-1$
   			}
   			sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$
   			print(sb.toString());
    	}

    	private void endTag(Element element) {
    		StringBuilder sb = new StringBuilder();
    		sb.append("</"); //$NON-NLS-1$
    		sb.append(element.getNodeName());
    		sb.append(">"); //$NON-NLS-1$
   			print(sb.toString());
    	}

    	private static void appendEscapedChar(StringBuilder buffer, char c) {
    		String replacement = getReplacement(c);
    		if (replacement != null) {
    			buffer.append('&');
    			buffer.append(replacement);
    			buffer.append(';');
    		} else if (c==9 || c==10 || c==13 || c>=32){
    			buffer.append(c);
    		}
    	}

    	private static String getEscaped(String s) {
    		StringBuilder result = new StringBuilder(s.length() + 10);
    		for (int i = 0; i < s.length(); ++i) {
				appendEscapedChar(result, s.charAt(i));
			}
    		return result.toString();
    	}

    	private static String getReplacement(char c) {
    		// Encode special XML characters into the equivalent character references.
			// The first five are defined by default for all XML documents.
			// The next three (#xD, #xA, #x9) are encoded to avoid them
			// being converted to spaces on deserialization
			// (fixes bug 93720)
    		switch (c) {
    			case '<' :
    				return "lt"; //$NON-NLS-1$
    			case '>' :
    				return "gt"; //$NON-NLS-1$
    			case '"' :
    				return "quot"; //$NON-NLS-1$
    			case '\'' :
    				return "apos"; //$NON-NLS-1$
    			case '&' :
    				return "amp"; //$NON-NLS-1$
				case '\r':
					return "#x0D"; //$NON-NLS-1$
				case '\n':
					return "#x0A"; //$NON-NLS-1$
				case '\u0009':
					return "#x09"; //$NON-NLS-1$
				default:
					break;
    		}
    		return null;
    	}
    }
}
