/*******************************************************************************
 * Copyright (c) 2001, 2006 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
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.ui.internal.contentassist;

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

import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
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.ITextRegionContainer;
import org.eclipse.wst.sse.core.internal.util.ScriptLanguageKeys;
import org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils;
import org.eclipse.wst.sse.ui.internal.contentassist.CustomCompletionProposal;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.eclipse.wst.xml.ui.internal.XMLUIMessages;
import org.eclipse.wst.xml.ui.internal.editor.XMLEditorPluginImageHelper;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

/**
 * @author pavery
 */
public class XMLContentAssistUtilities extends ContentAssistUtils {

	/**
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	private class DOMJSPRegionContextsPrivateCopy {
		private static final String JSP_CLOSE = "JSP_CLOSE"; //$NON-NLS-1$
		private static final String JSP_DECLARATION_OPEN = "JSP_DECLARATION_OPEN"; //$NON-NLS-1$
		private static final String JSP_SCRIPTLET_OPEN = "JSP_SCRIPTLET_OPEN"; //$NON-NLS-1$
		private static final String JSP_EXPRESSION_OPEN = "JSP_EXPRESSION_OPEN"; //$NON-NLS-1$

	}


	public static final String CONTENT = "Content"; //$NON-NLS-1$
	public static final String CONTENT_SCRIPT_TYPE = "Content-Script-Type"; //$NON-NLS-1$
	public static final String HEAD = "HEAD"; //$NON-NLS-1$
	public static final String HTML = "HTML"; //$NON-NLS-1$
	public static final String HTTP_EQUIV = "HTTP-EQUIV"; //$NON-NLS-1$
	public static final String META = "META"; //$NON-NLS-1$

	/**
	 * A convenience method for getting the closing proposal given the
	 * contents (IndexedRegion) of a tag that is started, but possibly not
	 * ended
	 * 
	 * @param viewer
	 *            the text viewer
	 * @param documentPosition
	 *            the cursor position in the viewer
	 * @param indexedNode
	 *            the contents of the tag that is started but possibly not
	 *            ended
	 * @param parentTagName
	 *            the tag on which you are checkin for an ending tag
	 * @param imagePath
	 *            content assist image relative path
	 * @return ICompletionProposal
	 */
	public static ICompletionProposal computeJSPEndTagProposal(ITextViewer viewer, int documentPosition, IndexedRegion indexedNode, String parentTagName, String imagePath) {
		ICompletionProposal p = null;

		// check if tag is closed
		boolean hasEndTag = true;
		boolean isJSPTag = false;
		IDOMNode xnode = null;
		String tagName = ""; //$NON-NLS-1$
		if (indexedNode instanceof IDOMNode) {
			xnode = ((IDOMNode) indexedNode);
			// it's ended already...
			if (xnode.getEndStructuredDocumentRegion() != null) {
				return null;
			}
			IDOMNode openNode = null;
			if (!xnode.getNodeName().equalsIgnoreCase(parentTagName)) {
				openNode = (IDOMNode) xnode.getParentNode();
			}
			if (openNode != null) {
				if (openNode instanceof IDOMElement) {
					isJSPTag = ((IDOMElement) openNode).isJSPTag();
				}
				tagName = openNode.getNodeName();
				hasEndTag = (openNode.getEndStructuredDocumentRegion() != null);
			}
		}

		// it's closed, don't add close tag proposal
		if (!hasEndTag && !isJSPTag) {

			// create appropriate close tag text
			String proposedText = "</" + tagName; //$NON-NLS-1$
			String viewerText = viewer.getTextWidget().getText();
			if ((viewerText.length() >= documentPosition) && (viewerText.length() >= 2) && (documentPosition >= 2)) {
				String last2chars = viewerText.substring(documentPosition - 2, documentPosition);
				if (last2chars.endsWith("</")) {
					proposedText = tagName;
				}
				else if (last2chars.endsWith("<")) {
					proposedText = "/" + tagName; //$NON-NLS-1$
				}
			}

			// create proposal
			p = new CustomCompletionProposal(proposedText + ">", //$NON-NLS-1$
						documentPosition, 0, proposedText.length() + 1, XMLEditorPluginImageHelper.getInstance().getImage(imagePath), NLS.bind(XMLUIMessages.End_with_, (new Object[]{proposedText})), null, null, XMLRelevanceConstants.R_END_TAG);
		}
		else if (!hasEndTag && isJSPTag) {

			// create appropriate close tag text
			String proposedText = "%"; //$NON-NLS-1$
			String viewerText = viewer.getTextWidget().getText();

			// TODO (pa) make it smarter to add "%>" or just ">" if % is
			// already there...
			if ((viewerText.length() >= documentPosition) && (viewerText.length() >= 2)) {
				String last2chars = viewerText.substring(documentPosition - 2, documentPosition);
				String lastchar = viewerText.substring(documentPosition - 1, documentPosition);
				if (lastchar.equals("%")) //$NON-NLS-1$
				{
					if (last2chars.endsWith("<%")) {
						proposedText = "%"; //$NON-NLS-1$
					}
					else {
						proposedText = ""; //$NON-NLS-1$
					}
				}
			}

			// create proposal
			p = new CustomCompletionProposal(proposedText + ">", //$NON-NLS-1$
						documentPosition, 0, proposedText.length() + 1, XMLEditorPluginImageHelper.getInstance().getImage(imagePath), NLS.bind(XMLUIMessages.End_with_, (new Object[]{proposedText})), null, null, XMLRelevanceConstants.R_END_TAG);
		}

		return p;
	}


	/**
	 * A convenience method for getting the closing proposal given the
	 * contents (IndexedRegion) of a tag that is started, but possibly not
	 * ended
	 * 
	 * @param viewer
	 *            the text viewer
	 * @param documentPosition
	 *            the cursor position in the viewer
	 * @param indexedNode
	 *            the contents of the tag that is started but possibly not
	 *            ended
	 * @param parentTagName
	 *            the tag on which you are checkin for an ending tag
	 * @param imagePath
	 *            content assist relative image path
	 * @return ICompletionProposal
	 */
	public static ICompletionProposal computeXMLEndTagProposal(ITextViewer viewer, int documentPosition, IndexedRegion indexedNode, String parentTagName, String imagePath) {
		ICompletionProposal p = null;

		// check if tag is closed
		boolean hasEndTag = true;
		IDOMNode xnode = null;
		String tagName = ""; //$NON-NLS-1$
		if (indexedNode instanceof IDOMNode) {
			xnode = ((IDOMNode) indexedNode);
			// it's ended already...
			if (xnode.getEndStructuredDocumentRegion() != null) {
				return null;
			}
			IDOMNode styleNode = null;
			if (!xnode.getNodeName().equalsIgnoreCase(parentTagName)) {
				styleNode = (IDOMNode) xnode.getParentNode();
			}
			if (styleNode != null) {
				tagName = styleNode.getNodeName();
				hasEndTag = (styleNode.getEndStructuredDocumentRegion() != null);
			}
		}

		// it's closed, don't add close tag proposal
		if (!hasEndTag) {

			// create appropriate close tag text
			String proposedText = "</" + tagName; //$NON-NLS-1$
			String viewerText = viewer.getTextWidget().getText();
			if ((viewerText.length() >= documentPosition) && (viewerText.length() >= 2) && (documentPosition >= 2)) {
				String last2chars = viewerText.substring(documentPosition - 2, documentPosition);
				if (last2chars.endsWith("</")) {
					proposedText = tagName;
				}
				else if (last2chars.endsWith("<")) {
					proposedText = "/" + tagName; //$NON-NLS-1$
				}
			}

			// create proposal
			p = new CustomCompletionProposal(proposedText + ">", //$NON-NLS-1$
						documentPosition, 0, proposedText.length() + 1, XMLEditorPluginImageHelper.getInstance().getImage(imagePath), NLS.bind(XMLUIMessages.End_with_, (new Object[]{proposedText})), null, null, XMLRelevanceConstants.R_END_TAG);
		}
		return p;
	}

	private static String getMetaScriptType(Document doc) {
		// Can not just do a Document.getElementsByTagName(String) as this
		// needs
		// to be relatively fast.
		List metas = new ArrayList();
		// check for META tags under the Document
		Node html = null;
		Node head = null;
		Node child = null;
		// ----------------------------------------------------------------------
		// (pa) 20021217
		// cmvc defect 235554
		// performance enhancement: using child.getNextSibling() rather than
		// nodeList(item) for O(n) vs. O(n*n)
		// ----------------------------------------------------------------------

		for (child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
			if (child.getNodeType() != Node.ELEMENT_NODE) {
				continue;
			}
			if (child.getNodeName().equalsIgnoreCase(META)) {
				metas.add(child);
			}
			else if (child.getNodeName().equalsIgnoreCase(HTML)) {
				html = child;
			}
		}
		// NodeList children = doc.getChildNodes();
		// for(int i = 0; i < children.getLength(); i++) {
		// child = children.item(i);
		// if(child.getNodeType() != Node.ELEMENT_NODE)
		// continue;
		// if(child.getNodeName().equalsIgnoreCase(META))
		// metas.add(child);
		// else if(child.getNodeName().equalsIgnoreCase(HTML))
		// html = child;
		// }

		// check for META tags under HEAD
		if (html != null) {
			for (child = html.getFirstChild(); (child != null) && (head == null); child = child.getNextSibling()) {
				if (child.getNodeType() != Node.ELEMENT_NODE) {
					continue;
				}
				if (child.getNodeName().equalsIgnoreCase(HEAD)) {
					head = child;
				}
			}
			// children = html.getChildNodes();
			// for(int i = 0; i < children.getLength() && head == null; i++) {
			// child = children.item(i);
			// if(child.getNodeType() != Node.ELEMENT_NODE)
			// continue;
			// if(child.getNodeName().equalsIgnoreCase(HEAD))
			// head = child;
			// }
		}

		if (head != null) {
			for (head.getFirstChild(); child != null; child = child.getNextSibling()) {
				if (child.getNodeType() != Node.ELEMENT_NODE) {
					continue;
				}
				if (child.getNodeName().equalsIgnoreCase(META)) {
					metas.add(child);
				}
			}
			// children = head.getChildNodes();
			// for(int i = 0 ; i < children.getLength(); i++) {
			// child = children.item(i);
			// if(child.getNodeType() != Node.ELEMENT_NODE)
			// continue;
			// if(child.getNodeName().equalsIgnoreCase(META))
			// metas.add(child);
			// }
		}

		return getMetaScriptType(metas);
	}

	private static String getMetaScriptType(List metaNodeList) {
		Node meta = null;
		NamedNodeMap attributes = null;
		boolean httpEquiv = false;
		String contentScriptType = null;

		for (int i = metaNodeList.size() - 1; i >= 0; i--) {
			meta = (Node) metaNodeList.get(i);
			attributes = meta.getAttributes();
			httpEquiv = false;
			contentScriptType = null;
			for (int j = 0; j < attributes.getLength(); j++) {
				if (attributes.item(j).getNodeName().equalsIgnoreCase(HTTP_EQUIV)) {
					httpEquiv = attributes.item(j).getNodeValue().equalsIgnoreCase(CONTENT_SCRIPT_TYPE);
				}
				else if (attributes.item(j).getNodeName().equalsIgnoreCase(CONTENT)) {
					contentScriptType = attributes.item(j).getNodeValue();
				}
			}
			if (httpEquiv && (contentScriptType != null)) {
				return contentScriptType;
			}
		}
		return null;
	}

	/**
	 * Returns the scripting language the scriptNode is in Currently returns
	 * javascript unless some unknown type or language is specified. Then the
	 * unknown type/language is returned
	 * 
	 * @param scriptNode
	 */
	public static String getScriptLanguage(Node scriptNode) {
		Node attr = null;

		boolean specified = false;
		// try to find a scripting adapter for 'type'
		if ((scriptNode == null) || (scriptNode.getAttributes() == null)) {
			return null;
		}

		attr = scriptNode.getAttributes().getNamedItem("type");//$NON-NLS-1$
		if (attr != null) {
			specified = true;
			String type = attr.getNodeValue();
			return lookupScriptType(type);
		}
		// now try to find a scripting adapter for 'language' (deprecated by
		// HTML specifications)
		attr = scriptNode.getAttributes().getNamedItem("language");//$NON-NLS-1$
		if (attr != null) {
			specified = true;
			String language = attr.getNodeValue();
			return lookupScriptLanguage(language);
		}
		// check if one is specified by a META tag at the root level or inside
		// of HEAD
		String type = null;
		if (!specified) {
			type = getMetaScriptType(scriptNode.getOwnerDocument());
		}
		if (type != null) {
			specified = true;
			return lookupScriptType(type);
		}
		// return default
		if (!specified) {
			return ScriptLanguageKeys.JAVASCRIPT;
		}
		return null;
	}

	/**
	 * Tells you if the flatnode is the %> delimiter
	 * 
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isJSPCloseDelimiter(IStructuredDocumentRegion fn) {
		if (fn == null) {
			return false;
		}
		return isJSPCloseDelimiter(fn.getType());
	}

	/**
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isJSPCloseDelimiter(String type) {
		if (type == null) {
			return false;
		}
		return (type.equals(DOMJSPRegionContextsPrivateCopy.JSP_CLOSE) || type.equals(DOMRegionContext.XML_TAG_CLOSE));
	}

	/**
	 * Tells you if the flatnode is the JSP region <%%>, <%=%>, <%!%>
	 * 
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isJSPDelimiter(IStructuredDocumentRegion fn) {
		boolean isDelimiter = false;
		String type = fn.getType();
		if (type != null) {
			isDelimiter = isJSPDelimiter(type);
		}
		return isDelimiter;
	}

	/**
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isJSPDelimiter(String type) {
		if (type == null) {
			return false;
		}
		return (isJSPOpenDelimiter(type) || isJSPCloseDelimiter(type));
	}

	/**
	 * Tells you if the flatnode is <%, <%=, or <%! ISSUE: this is a bit of
	 * hidden JSP knowledge that was implemented this way for expedency.
	 * Should be evolved in future to depend on "nestedContext".
	 */
	public static boolean isJSPOpenDelimiter(IStructuredDocumentRegion fn) {
		if (fn == null) {
			return false;
		}
		return isJSPOpenDelimiter(fn.getType());
	}

	/**
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isJSPOpenDelimiter(String type) {
		if (type == null) {
			return false;
		}
		return (type.equals(DOMJSPRegionContextsPrivateCopy.JSP_SCRIPTLET_OPEN) || type.equals(DOMJSPRegionContextsPrivateCopy.JSP_DECLARATION_OPEN) || type.equals(DOMJSPRegionContextsPrivateCopy.JSP_EXPRESSION_OPEN));
	}

	/**
	 * Tells you if the flatnode is the <jsp:scriptlet>, <jsp:expression>, or
	 * <jsp:declaration>tag
	 * 
	 * ISSUE: this is a bit of hidden JSP knowledge that was implemented this
	 * way for expedency. Should be evolved in future to depend on
	 * "nestedContext".
	 */
	public static boolean isXMLJSPDelimiter(IStructuredDocumentRegion fn) {
		boolean isDelimiter = false;
		if ((fn != null) && (fn instanceof ITextRegionContainer)) {
			Object[] regions = ((ITextRegionContainer) fn).getRegions().toArray();
			ITextRegion temp = null;
			String regionText = ""; //$NON-NLS-1$
			for (int i = 0; i < regions.length; i++) {
				temp = (ITextRegion) regions[i];
				if (temp.getType() == DOMRegionContext.XML_TAG_NAME) {
					regionText = fn.getText(temp);
					if (regionText.equalsIgnoreCase("jsp:scriptlet") || regionText.equalsIgnoreCase("jsp:expression") || regionText.equalsIgnoreCase("jsp:declaration")) {
						isDelimiter = true;
					}
				}
			}
		}
		return isDelimiter;
	}

	/**
	 * Returns "javascript" if language attribute is some form of javascript,
	 * "java" if language attribute is some form of java. Otherwise, just
	 * returns type.
	 * 
	 * @param language
	 */
	public static String lookupScriptLanguage(String language) {
		for (int i = 0; i < ScriptLanguageKeys.JAVASCRIPT_LANGUAGE_KEYS.length; i++) {
			if (ScriptLanguageKeys.JAVASCRIPT_LANGUAGE_KEYS[i].equalsIgnoreCase(language)) {
				return ScriptLanguageKeys.JAVASCRIPT;
			}
		}
		for (int i = 0; i < ScriptLanguageKeys.JAVA_LANGUAGE_KEYS.length; i++) {
			if (ScriptLanguageKeys.JAVA_LANGUAGE_KEYS[i].equalsIgnoreCase(language)) {
				return ScriptLanguageKeys.JAVA;
			}
		}
		return language;
	}

	/**
	 * Returns "javascript" if type (used in <script type="xxx"> is actually
	 * javascript type. Otherwise, just returns type
	 * 
	 * @param type
	 */
	public static String lookupScriptType(String type) {
		for (int i = 0; i < ScriptLanguageKeys.JAVASCRIPT_MIME_TYPE_KEYS.length; i++) {
			if (ScriptLanguageKeys.JAVASCRIPT_MIME_TYPE_KEYS[i].equalsIgnoreCase(type)) {
				return ScriptLanguageKeys.JAVASCRIPT;
			}
		}
		return type;
	}
}
