/*******************************************************************************
 * Copyright (c) 2001, 2010 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     David Carver (Intalion) - Cleanup Repeated Conditional check in isXMLType method
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.cleanup;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.wst.sse.core.internal.cleanup.IStructuredCleanupHandler;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.provisional.document.ISourceGenerator;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
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;


public class ElementNodeCleanupHandler extends NodeCleanupHandler {
	protected static final char DOUBLE_QUOTE = '\"'; //$NON-NLS-1$
	protected static final String DOUBLE_QUOTES = "\"\""; //$NON-NLS-1$
	protected static final String EMPTY_TAG_CLOSE = "/>"; //$NON-NLS-1$
	protected static final String END_TAG_OPEN = "</"; //$NON-NLS-1$
	protected static final char SINGLE_QUOTE = '\''; //$NON-NLS-1$
	protected static final String SINGLE_QUOTES = "''"; //$NON-NLS-1$

	/** Non-NLS strings */
	protected static final String START_TAG_OPEN = "<"; //$NON-NLS-1$
	protected static final String TAG_CLOSE = ">"; //$NON-NLS-1$

	public Node cleanup(Node node) {
		Node newNode = cleanupChildren(node);
		IDOMNode renamedNode = newNode instanceof IDOMNode ? (IDOMNode) newNode : null;

		// call quoteAttrValue() first so it will close any unclosed attr
		// quoteAttrValue() will return the new start tag if there is a
		// structure change
		renamedNode = quoteAttrValue(renamedNode);

		// insert tag close if missing
		// if node is not comment tag
		// and not implicit tag
		if (!isCommentTag(renamedNode) && !isImplicitTag(renamedNode)) {
			IDOMModel structuredModel = renamedNode.getModel();

			// save start offset before insertTagClose()
			// or else renamedNode.getStartOffset() will be zero if
			// renamedNode replaced by insertTagClose()
			int startTagStartOffset = renamedNode.getStartOffset();

			// for start tag
			IStructuredDocumentRegion startTagStructuredDocumentRegion = renamedNode.getStartStructuredDocumentRegion();
			insertTagClose(structuredModel, startTagStructuredDocumentRegion);

			// update renamedNode and startTagStructuredDocumentRegion after
			// insertTagClose()
			renamedNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset);
			startTagStructuredDocumentRegion = renamedNode.getStartStructuredDocumentRegion();

			// for end tag
			IStructuredDocumentRegion endTagStructuredDocumentRegion = renamedNode.getEndStructuredDocumentRegion();
			if (endTagStructuredDocumentRegion != startTagStructuredDocumentRegion)
				insertTagClose(structuredModel, endTagStructuredDocumentRegion);
		}

		// call insertMissingTags() next, it will generate implicit tags if
		// there are any
		// insertMissingTags() will return the new missing start tag if one is
		// missing
		renamedNode = insertMissingTags(renamedNode);

		renamedNode = compressEmptyElementTag(renamedNode);

		renamedNode = insertRequiredAttrs(renamedNode);

		return renamedNode;
	}

	protected Node cleanupChildren(Node node) {
		Node parentNode = node;

		if (node != null) {
			Node childNode = node.getFirstChild();
			while (childNode != null) {
				// get cleanup handler
				IStructuredCleanupHandler cleanupHandler = getCleanupHandler(childNode);

				// cleanup each child
				childNode = cleanupHandler.cleanup(childNode);

				// get new parent node
				parentNode = childNode.getParentNode();

				// get next child node
				childNode = childNode.getNextSibling();
			}
		}

		return parentNode;
	}

	private IDOMNode compressEmptyElementTag(IDOMNode node) {
		boolean compressEmptyElementTags = getCleanupPreferences().getCompressEmptyElementTags();
		IDOMNode newNode = node;

		IStructuredDocumentRegion startTagStructuredDocumentRegion = newNode.getFirstStructuredDocumentRegion();
		IStructuredDocumentRegion endTagStructuredDocumentRegion = newNode.getLastStructuredDocumentRegion();

		if (compressEmptyElementTags && startTagStructuredDocumentRegion != endTagStructuredDocumentRegion && startTagStructuredDocumentRegion != null) {
			ITextRegionList regions = startTagStructuredDocumentRegion.getRegions();
			ITextRegion lastRegion = regions.get(regions.size() - 1);
			// format children and end tag if not empty element tag
			if (lastRegion.getType() != DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
				NodeList childNodes = newNode.getChildNodes();
				if (childNodes == null || childNodes.getLength() == 0 || (childNodes.getLength() == 1 && (childNodes.item(0)).getNodeType() == Node.TEXT_NODE && ((childNodes.item(0)).getNodeValue().trim().length() == 0))) {
					IDOMModel structuredModel = newNode.getModel();
					IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();

					int startTagStartOffset = newNode.getStartOffset();
					int offset = endTagStructuredDocumentRegion.getStart();
					int length = endTagStructuredDocumentRegion.getLength();
					structuredDocument.replaceText(structuredDocument, offset, length, ""); //$NON-NLS-1$
					newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save

					offset = startTagStructuredDocumentRegion.getStart() + lastRegion.getStart();
					structuredDocument.replaceText(structuredDocument, offset, 0, "/"); //$NON-NLS-1$
					newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
				}
			}
		}

		return newNode;
	}

	protected IStructuredCleanupHandler getCleanupHandler(Node node) {
		short nodeType = node.getNodeType();
		IStructuredCleanupHandler cleanupHandler = null;
		switch (nodeType) {
			case org.w3c.dom.Node.ELEMENT_NODE : {
				cleanupHandler = new ElementNodeCleanupHandler();
				break;
			}
			case org.w3c.dom.Node.TEXT_NODE : {
				cleanupHandler = new NodeCleanupHandler();
				break;
			}
			default : {
				cleanupHandler = new NodeCleanupHandler();
			}
		}

		// init CleanupPreferences
		cleanupHandler.setCleanupPreferences(getCleanupPreferences());

		return cleanupHandler;
	}


	protected ModelQuery getModelQuery(Node node) {
		if (node.getNodeType() == Node.DOCUMENT_NODE) {
			return ModelQueryUtil.getModelQuery((Document) node);
		} else {
			return ModelQueryUtil.getModelQuery(node.getOwnerDocument());
		}
	}

	protected List getRequiredAttrs(Node node) {
		List result = new ArrayList();

		ModelQuery modelQuery = getModelQuery(node);
		if (modelQuery != null) {
			CMElementDeclaration elementDecl = modelQuery.getCMElementDeclaration((Element) node);
			if (elementDecl != null) {
				CMNamedNodeMap attrMap = elementDecl.getAttributes();
				Iterator it = attrMap.iterator();
				CMAttributeDeclaration attr = null;
				while (it.hasNext()) {
					attr = (CMAttributeDeclaration) it.next();
					if (attr.getUsage() == CMAttributeDeclaration.REQUIRED) {
						result.add(attr);
					}
				}
			}
		}

		return result;
	}

	private IDOMNode insertEndTag(IDOMNode node) {
		IDOMNode newNode = node;
		IDOMElement element = (IDOMElement) node;
		if (element.isCommentTag())
			return node; // do nothing

		int startTagStartOffset = node.getStartOffset();
		IDOMModel structuredModel = node.getModel();

		if (isEmptyElement(element)) {
			IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
			IStructuredDocumentRegion startStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
			ITextRegionList regions = startStructuredDocumentRegion.getRegions();
			ITextRegion lastRegion = regions.get(regions.size() - 1);
			structuredDocument.replaceText(structuredDocument, startStructuredDocumentRegion.getStartOffset(lastRegion), lastRegion.getLength(), EMPTY_TAG_CLOSE);

			if (regions.size() > 1) {
				ITextRegion regionBeforeTagClose = regions.get(regions.size() - 1 - 1);

				// insert a space separator before tag close if the previous
				// region does not have extra spaces
				if (regionBeforeTagClose.getTextLength() == regionBeforeTagClose.getLength())
					structuredDocument.replaceText(structuredDocument, startStructuredDocumentRegion.getStartOffset(lastRegion), 0, " "); //$NON-NLS-1$
			}
		} else {
			String tagName = node.getNodeName();
			String endTag = END_TAG_OPEN.concat(tagName).concat(TAG_CLOSE);

			IDOMNode lastChild = (IDOMNode) node.getLastChild();
			int endTagStartOffset = 0;
			if (lastChild != null)
				// if this node has children, insert the end tag after the
				// last child
				endTagStartOffset = lastChild.getEndOffset();
			else
				// if this node does not has children, insert the end tag
				// after the start tag
				endTagStartOffset = node.getEndOffset();

			IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
			structuredDocument.replaceText(structuredDocument, endTagStartOffset, 0, endTag);
		}

		newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
		// new
		// node

		return newNode;
	}

	private IDOMNode insertMissingTags(IDOMNode node) {
		boolean insertMissingTags = getCleanupPreferences().getInsertMissingTags();
		IDOMNode newNode = node;

		if (insertMissingTags) {
			IStructuredDocumentRegion startTagStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
			if (startTagStructuredDocumentRegion == null) {
				// implicit start tag; generate tag for it
				newNode = insertStartTag(node);
				startTagStructuredDocumentRegion = newNode.getStartStructuredDocumentRegion();
			}

			IStructuredDocumentRegion endTagStructuredDocumentRegion = newNode.getEndStructuredDocumentRegion();
			ITextRegionList startStructuredDocumentRegionRegions = startTagStructuredDocumentRegion.getRegions();
			if (startTagStructuredDocumentRegion != null && startStructuredDocumentRegionRegions != null && (startStructuredDocumentRegionRegions.get(startStructuredDocumentRegionRegions.size() - 1)).getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE) {

			} else {
				if (startTagStructuredDocumentRegion == null) {
					// start tag missing
					if (isStartTagRequired(newNode))
						newNode = insertStartTag(newNode);
				} else if (endTagStructuredDocumentRegion == null) {
					// end tag missing
					if (isEndTagRequired(newNode))
						newNode = insertEndTag(newNode);
				}
			}
		}

		return newNode;
	}

	private IDOMNode insertRequiredAttrs(IDOMNode node) {
		boolean insertRequiredAttrs = getCleanupPreferences().getInsertRequiredAttrs();
		IDOMNode newNode = node;

		if (insertRequiredAttrs) {
			List requiredAttrs = getRequiredAttrs(newNode);
			if (requiredAttrs.size() > 0) {
				NamedNodeMap currentAttrs = node.getAttributes();
				List insertAttrs = new ArrayList();
				if (currentAttrs.getLength() == 0)
					insertAttrs.addAll(requiredAttrs);
				else {
					for (int i = 0; i < requiredAttrs.size(); i++) {
						String requiredAttrName = ((CMAttributeDeclaration) requiredAttrs.get(i)).getAttrName();
						boolean found = false;
						for (int j = 0; j < currentAttrs.getLength(); j++) {
							String currentAttrName = currentAttrs.item(j).getNodeName();
							if (requiredAttrName.compareToIgnoreCase(currentAttrName) == 0) {
								found = true;
								break;
							}
						}
						if (!found)
							insertAttrs.add(requiredAttrs.get(i));
					}
				}
				if (insertAttrs.size() > 0) {
					IStructuredDocumentRegion startStructuredDocumentRegion = newNode.getStartStructuredDocumentRegion();
					int index = startStructuredDocumentRegion.getEndOffset();
					ITextRegion lastRegion = startStructuredDocumentRegion.getLastRegion();
					if (lastRegion.getType() == DOMRegionContext.XML_TAG_CLOSE) {
						index--;
						lastRegion = startStructuredDocumentRegion.getRegionAtCharacterOffset(index - 1);
					} else if (lastRegion.getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
						index = index - 2;
						lastRegion = startStructuredDocumentRegion.getRegionAtCharacterOffset(index - 1);
					}
					MultiTextEdit multiTextEdit = new MultiTextEdit();
					try {
						for (int i = insertAttrs.size() - 1; i >= 0; i--) {
							CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) insertAttrs.get(i);
							String requiredAttributeName = attrDecl.getAttrName();
							String defaultValue = attrDecl.getDefaultValue();
							if (defaultValue == null)
								defaultValue = ""; //$NON-NLS-1$
							String nameAndDefaultValue = " "; //$NON-NLS-1$
							if (i == 0 && lastRegion.getLength() > lastRegion.getTextLength())
								nameAndDefaultValue = ""; //$NON-NLS-1$
							nameAndDefaultValue += requiredAttributeName + "=\"" + defaultValue + "\""; //$NON-NLS-1$ //$NON-NLS-2$
							multiTextEdit.addChild(new InsertEdit(index, nameAndDefaultValue));
							// BUG3381: MultiTextEdit applies all child
							// TextEdit's basing on offsets
							//          in the document before the first TextEdit, not
							// after each
							//          child TextEdit. Therefore, do not need to
							// advance the index.
							//index += nameAndDefaultValue.length();
						}
						multiTextEdit.apply(newNode.getStructuredDocument());
					} catch (BadLocationException e) {
						// log for now, unless we find reason not to
						Logger.log(Logger.INFO, e.getMessage());
					}
				}
			}
		}

		return newNode;
	}

	private IDOMNode insertStartTag(IDOMNode node) {
		IDOMNode newNode = node;

		if (isCommentTag(node))
			return node; // do nothing

		String tagName = node.getNodeName();
		String startTag = START_TAG_OPEN.concat(tagName).concat(TAG_CLOSE);
		int startTagStartOffset = node.getStartOffset();

		IDOMModel structuredModel = node.getModel();
		IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
		structuredDocument.replaceText(structuredDocument, startTagStartOffset, 0, startTag);
		newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
		// new
		// node

		return newNode;
	}

	private void insertTagClose(IDOMModel structuredModel, IStructuredDocumentRegion flatNode) {
		if (flatNode != null) {
			ITextRegionList flatnodeRegions = flatNode.getRegions();
			if (flatnodeRegions != null) {
				ITextRegion lastRegion = flatnodeRegions.get(flatnodeRegions.size() - 1);
				if (lastRegion != null) {
					String regionType = lastRegion.getType();
					if ((regionType != DOMRegionContext.XML_EMPTY_TAG_CLOSE) && (regionType != DOMRegionContext.XML_TAG_CLOSE)) {
						IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();

						// insert ">" after lastRegion of flatNode
						// as in "<a</a>" if flatNode is for start tag, or in
						// "<a></a" if flatNode is for end tag
						structuredDocument.replaceText(structuredDocument, flatNode.getTextEndOffset(lastRegion), 0, ">"); //$NON-NLS-1$
					}
				}
			}
		}
	}

	/**
	 * @param renamedNode
	 * @return
	 */
	private boolean isCommentTag(Node renamedNode) {
		boolean result = false;
		if (renamedNode instanceof IDOMElement) {
			IDOMElement element = (IDOMElement) renamedNode;
			result = element.isCommentTag();
		}
		return result;
	}

	private boolean isEmptyElement(IDOMElement element) {
		Document document = element.getOwnerDocument();
		if (document == null)
			// undefined tag, return default
			return false;

		ModelQuery modelQuery = ModelQueryUtil.getModelQuery(document);
		if (modelQuery == null)
			// undefined tag, return default
			return false;

		CMElementDeclaration decl = modelQuery.getCMElementDeclaration(element);
		if (decl == null)
			// undefined tag, return default
			return false;

		return (decl.getContentType() == CMElementDeclaration.EMPTY);
	}

	private boolean isEndTagRequired(IDOMNode node) {
		if (node == null)
			return false;
		return node.isContainer();
	}

	/**
	 * A tag is implicit if it has not corresponding region in document.
	 * 
	 * @param renamedNode
	 * @return
	 */
	private boolean isImplicitTag(IDOMNode renamedNode) {
		return renamedNode.getStartStructuredDocumentRegion() == null;
	}

	/**
	 * The end tags of HTML EMPTY content type, such as IMG, and HTML
	 * undefined tags are parsed separately from the start tags. So inserting
	 * the missing start tag is useless and even harmful.
	 */
	private boolean isStartTagRequired(IDOMNode node) {
		if (node == null)
			return false;
		return node.isContainer();
	}

	private boolean isXMLType(IDOMModel structuredModel) {
		boolean result = false;

		if (structuredModel != null) {
			IDOMDocument document = structuredModel.getDocument();

			if (document != null)
				result = document.isXMLType();
		}

		return result;
	}

	private IDOMNode quoteAttrValue(IDOMNode node) {
		IDOMNode newNode = node;
		//XMLElement element = (XMLElement) node;
		if (isCommentTag(node))
			return node; // do nothing

		boolean quoteAttrValues = getCleanupPreferences().getQuoteAttrValues();

		if (quoteAttrValues) {
			NamedNodeMap attributes = newNode.getAttributes();
			if (attributes != null) {
				int attributesLength = attributes.getLength();
				ISourceGenerator generator = node.getModel().getGenerator();

				for (int i = 0; i < attributesLength; i++) {
					attributes = newNode.getAttributes();
					attributesLength = attributes.getLength();
					IDOMAttr eachAttr = (IDOMAttr) attributes.item(i);
					//ITextRegion oldAttrValueRegion =
					// eachAttr.getValueRegion();
					String oldAttrValue = eachAttr.getValueRegionText();
					if (oldAttrValue == null) {
						IDOMModel structuredModel = node.getModel();
						if (isXMLType(structuredModel)) {
							String newAttrValue = "\"" + eachAttr.getNameRegionText() + "\""; //$NON-NLS-1$ //$NON-NLS-2$

							IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
							if (eachAttr.getEqualRegion() != null)
								// equal region exists
								structuredDocument.replaceText(structuredDocument, eachAttr.getEndOffset(), 0, newAttrValue);
							else
								// no equal region
								structuredDocument.replaceText(structuredDocument, eachAttr.getNameRegionTextEndOffset(), 0, "=".concat(newAttrValue)); //$NON-NLS-1$
							newNode = (IDOMNode) structuredModel.getIndexedRegion(node.getStartOffset()); // save
							// new
							// node
						}
					} else {
						//String oldAttrValue = oldAttrValueRegion.getText();
						char quote = StringUtils.isQuoted(oldAttrValue) ? oldAttrValue.charAt(0) : DOUBLE_QUOTE;
						String newAttrValue = generator.generateAttrValue(eachAttr, quote);

						// There is a problem in
						// StructuredDocumentRegionUtil.getAttrValue(ITextRegion)
						// when the region is instanceof ContextRegion.
						// Workaround for now...
						if (oldAttrValue.length() == 1) {
							char firstChar = oldAttrValue.charAt(0);
							if (firstChar == SINGLE_QUOTE)
								newAttrValue = SINGLE_QUOTES;
							else if (firstChar == DOUBLE_QUOTE)
								newAttrValue = DOUBLE_QUOTES;
						}

						if (newAttrValue != null) {
							if (newAttrValue.compareTo(oldAttrValue) != 0) {
								int attrValueStartOffset = eachAttr.getValueRegionStartOffset();
								int attrValueLength = oldAttrValue.length();
								int startTagStartOffset = node.getStartOffset();

								IDOMModel structuredModel = node.getModel();
								IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
								structuredDocument.replaceText(structuredDocument, attrValueStartOffset, attrValueLength, newAttrValue);
								newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
								// new
								// node
							}
						}
					}
				}
			}
		}

		return newNode;
	}
}
