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

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.AnnotationInfo;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.IncrementalReporter;
import org.eclipse.wst.validation.internal.core.IMessageAccess;
import org.eclipse.wst.validation.internal.core.ValidationException;
import org.eclipse.wst.validation.internal.provisional.core.IMessage;
import org.eclipse.wst.validation.internal.provisional.core.IProjectValidationContext;
import org.eclipse.wst.validation.internal.provisional.core.IReporter;
import org.eclipse.wst.validation.internal.provisional.core.IValidationContext;
import org.eclipse.wst.validation.internal.provisional.core.IValidator;
import org.eclipse.wst.validation.internal.provisional.core.IValidatorJob;
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.IDOMText;
import org.eclipse.wst.xml.core.internal.validation.AnnotationMsg;
import org.eclipse.wst.xml.ui.internal.Logger;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A DelegatingReconcileValidator calls its delegate validator to get a list
 * of validation error IMessages. Using information in this IMessage the
 * DelegatingReconcileValidator updates the IMessage with an offset and length
 * to give a good range to be "squiggled" and adds the messages to the
 * IReporter
 * 
 * @author Mark Hutchinson
 * 
 */
public abstract class DelegatingSourceValidator implements IValidator {
	// the selection strategies:
	protected static final String ALL_ATTRIBUTES = "ALL_ATTRIBUTES"; //$NON-NLS-1$
	protected static final String ATTRIBUTE_NAME = "ATTRIBUTE_NAME"; //$NON-NLS-1$
	protected static final String ATTRIBUTE_NAME_LAST = "ATTRIBUTE_NAME_LAST"; //$NON-NLS-1$
	protected static final String ATTRIBUTE_VALUE = "ATTRIBUTE_VALUE"; //$NON-NLS-1$
	protected static final String START_TAG = "START_TAG"; //$NON-NLS-1$
	protected static final String TEXT = "TEXT"; //$NON-NLS-1$
	protected static final String FIRST_NON_WHITESPACE_TEXT = "FIRST_NON_WHITESPACE_TEXT"; //$NON-NLS-1$
	protected static final String TEXT_ENTITY_REFERENCE = "TEXT_ENTITY_REFERENCE"; //$NON-NLS-1$
	protected static final String VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE = "VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE"; //$NON-NLS-1$
	protected static final String END_TAG = "END_TAG"; //$NON-NLS-1$
	private static final String QUICKASSISTPROCESSOR = IQuickAssistProcessor.class.getName();
	


	/**
	 * This constant specifies the attribute name that specifies the side of
	 * the 'start tag' that the validator has used to report an error. A
	 * validator may choose to report a message at the left (the start of the
	 * start tag)or at the right (the end of the start tag). When this
	 * attribute is not specified error ERROR_SIDE_LEFT is the default.
	 */
	protected static final String ERROR_SIDE = "ERROR_SIDE"; //$NON-NLS-1$

	/**
	 * When the ERROR_SIDE attribute specifies the ERROR_SIDE_LEFT value it is
	 * assumed that the message specifies a location to the left of the start
	 * tag
	 */
	protected static final String ERROR_SIDE_LEFT = "ERROR_SIDE_LEFT"; //$NON-NLS-1$	

	/**
	 * When the ERROR_SIDE attribute specifies the ERROR_SIDE_RIGHT value it
	 * is assumed that the message specifies a location to the right of the
	 * start tag
	 */
	protected static final String ERROR_SIDE_RIGHT = "ERROR_SIDE_RIGHT"; //$NON-NLS-1$	

	protected static final String COLUMN_NUMBER_ATTRIBUTE = "columnNumber"; //$NON-NLS-1$
	protected static final String SQUIGGLE_SELECTION_STRATEGY_ATTRIBUTE = "squiggleSelectionStrategy"; //$NON-NLS-1$
	protected static final String SQUIGGLE_NAME_OR_VALUE_ATTRIBUTE = "squiggleNameOrValue"; //$NON-NLS-1$

	public DelegatingSourceValidator() {
		super(); // constructor
	}

	public void cleanup(IReporter arg0) { // don't need to implement
	}

	// My Implementation of IHelper
	class MyHelper implements IProjectValidationContext {
		InputStream inputStream;

		IFile file;

		public MyHelper(InputStream inputStream, IFile file) {
			this.inputStream = inputStream;
			this.file = file;
		}

		public int getBuildKind() {
			return 0;
		}

		public Object loadModel(String symbolicName, Object[] parms) {
			if (symbolicName.equals("getFile")) { //$NON-NLS-1$
				return file;
			}
			return null;
		}

		public Object loadModel(String symbolicName) {
			if (symbolicName.equals("inputStream")) { //$NON-NLS-1$
				return inputStream;
			}
			return null;
		}

		public String[] getURIs() {
			if (file != null) {
				return new String[]{file.getFullPath().toString()};
			}
			return new String[0];
		}

		public IProject getProject() {
			if (file != null) {
				return file.getProject();
			}
			return null;
		}
	}

	// My Implementation of IReporter
	class MyReporter implements IReporter {
		List list = new ArrayList();

		public MyReporter() {
			super();
		}

		public void addMessage(IValidator origin, IMessage message) {
			list.add(message);
		}

		public void displaySubtask(IValidator validator, IMessage message) {
			/* do not need to implement */
		}

		public IMessageAccess getMessageAccess() {
			return null;
		}

		public boolean isCancelled() {
			return false;
		}

		public void removeAllMessages(IValidator origin, Object object) { // do
			/* do not need to implement */
		}

		public void removeAllMessages(IValidator origin) {
			/* do not need to implement */
		}

		public void removeMessageSubset(IValidator validator, Object obj, String groupName) {// do
			/* do not need to implement */
		}

		public List getMessages() {
			return list;
		}
	}

	protected abstract IValidator getDelegateValidator();

	/**
	 * Calls a delegate validator getting and updates it's list of
	 * ValidationMessages with a good squiggle offset and length.
	 * 
	 * @param helper
	 *            loads an object.
	 * @param reporter
	 *            Is an instance of an IReporter interface, which is used for
	 *            interaction with the user.
	 */
	public void validate(IValidationContext helper, IReporter reporter) throws ValidationException {
		String[] delta = helper.getURIs();
		if (delta.length > 0) {
			// get the file, model and document:
			IFile file = getFile(delta[0]);
			IDOMModel xmlModel = null;
			if (file != null)
				xmlModel = getModelForResource(file);
			// some problem occurred, abort
			if (xmlModel == null)
				return;
			
			try {
				IDOMDocument document = xmlModel.getDocument();

				// store the text in a byte array; make a full copy to ease
				// any threading problems
				byte[] byteArray;
				try {
					byteArray = xmlModel.getStructuredDocument().get().getBytes("UTF-8");
				}
				catch (UnsupportedEncodingException e) {
					// Not likely to happen
					byteArray = xmlModel.getStructuredDocument().get().getBytes();
				}

				if (isDelegateValidatorEnabled(file)) {
					IValidator validator = getDelegateValidator();
					if (validator != null) {
						// Validate the file:
						IValidationContext vHelper = new MyHelper(new ByteArrayInputStream(byteArray), file);
						MyReporter vReporter = new MyReporter();
						if (validator instanceof IValidatorJob) {
							((IValidatorJob) validator).validateInJob(vHelper, vReporter);
						}
						else {
							validator.validate(vHelper, vReporter);
						}
						List messages = vReporter.list;

						// set the offset and length
						updateValidationMessages(messages, document, reporter);
					}
				}
			}


			finally {
				if (xmlModel != null) {
					xmlModel.releaseFromRead();
				}
			}
		}
	}

	/**
	 * iterates through the messages and calculates a "better" offset and
	 * length
	 * 
	 * @param messages -
	 *            a List of IMessages
	 * @param document -
	 *            the document
	 * @param reporter -
	 *            the reporter the messages are to be added to
	 */
	protected void updateValidationMessages(List messages, IDOMDocument document, IReporter reporter) {
		for (int i = 0; i < messages.size(); i++) {
			IMessage message = (IMessage) messages.get(i);
			try {
				if (message.getAttribute(COLUMN_NUMBER_ATTRIBUTE) != null) {
					int column = ((Integer) message.getAttribute(COLUMN_NUMBER_ATTRIBUTE)).intValue();
					if (message.getAttribute(AnnotationMsg.PROBMLEM_ID) != null && reporter instanceof IncrementalReporter){
						Integer problemId = (Integer)message.getAttribute(AnnotationMsg.PROBMLEM_ID);
						MarkupQuickAssistProcessor processor = new MarkupQuickAssistProcessor();
						processor.setProblemId(problemId.intValue());
						message.setOffset(column);
						Integer length = (Integer) message.getAttribute(AnnotationMsg.LENGTH);
						message.setLength( length.intValue());
						Object attrValue = message.getAttribute(AnnotationMsg.ATTRVALUETEXT);
						if (attrValue != null)
							processor.setAdditionalFixInfo(attrValue);
						else{
							Object attrValueNo = message.getAttribute(AnnotationMsg.ATTRVALUENO);
							if (attrValueNo != null){
								int len = ((Integer)attrValueNo).intValue();
								Object[] objArray = new Object[len];
								for ( int j=0; j<len; j++){
									objArray[j] = message.getAttribute(AnnotationMsg.ATTRNO + j);
								}
								processor.setAdditionalFixInfo(objArray);
							}
							
						}
						message.setAttribute(QUICKASSISTPROCESSOR, processor);
						AnnotationInfo info = new AnnotationInfo(message);
						((IncrementalReporter) reporter).addAnnotationInfo(this, info);
					}
					else  {
						String selectionStrategy = (String) message.getAttribute(SQUIGGLE_SELECTION_STRATEGY_ATTRIBUTE);
						String nameOrValue = (String) message.getAttribute(SQUIGGLE_NAME_OR_VALUE_ATTRIBUTE);

						// convert the line and Column numbers to an offset:
						int start = document.getStructuredDocument().getLineOffset(message.getLineNumber() - 1) + column - 1;
						int[] result = computeStartAndEndLocation(start, selectionStrategy, getErrorSide(message), nameOrValue, document);
						if (result != null) {
							message.setOffset(result[0]);
							message.setLength(result[1] - result[0]);
						
							reporter.addMessage(this, message);
						}
					}
					}
				
			}
			catch (BadLocationException e) { // this exception should not
				// occur - it is thrown if
				// trying to convert an
				// invalid line number to and
				// offset
			}

		}
	}

	/**
	 * @param delta
	 *            the IFileDelta containing the file name to get
	 * @return the IFile
	 */
	public IFile getFile(String delta) {
		IPath path = new Path(delta);
		if (path.segmentCount() > 1) {
		  IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path);
		  if (file != null && file.exists())
		    return file;
		}
		return null;
	}

	/**
	 * 
	 * @param file
	 *            the file to get the model for
	 * @return the file's XMLModel or null
	 */
	protected IDOMModel getModelForResource(IFile file) {
		IStructuredModel model = null;
		IModelManager manager = StructuredModelManager.getModelManager();

		try {
			model = manager.getModelForRead(file);
			// TODO.. HTML validator tries again to get a model a 2nd way
		}
		catch (Exception e) {
			Logger.log(Logger.ERROR_DEBUG, file.getFullPath().toString(), e);
		}

		if (model instanceof IDOMModel)
			return (IDOMModel) model;
		if (model != null)
			model.releaseFromRead();
		return null;
	}

	/**
	 * @deprecated use computeStartEndLocation(int startOffset, String
	 *             errorMessage, String selectionStrategy, boolean leftError,
	 *             String nameOrValue, IDOMDocument document) {
	 * 
	 */
	protected int[] computeStartEndLocation(int startOffset, String errorMessage, String selectionStrategy, String nameOrValue, IDOMDocument document) {
		return computeStartAndEndLocation(startOffset, selectionStrategy, ERROR_SIDE_RIGHT, nameOrValue, document);
	}

	/**
	 * Calculates the "better" offsets.
	 * 
	 * @param startOffset -
	 *            the offset given by Xerces
	 * @param errorMessage -
	 *            the Xerces error Message
	 * @param selectionStrategy -
	 *            the selectionStrategy
	 * @param document -
	 *            the document
	 * @return int[] - position 0 has the start offset of the squiggle range,
	 *         position 1 has the endOffset
	 */
	/*
	 * The way the offsets is calculated is: - find the indexed region
	 * (element) closest to the given offset - if we are between two elements,
	 * choosing left or right element will depend on parameter 'errorSide' -
	 * based on the selectionStrategy choose the underlining strategy (eg
	 * START_TAG means underline the start tag of that element) - use
	 * information from nameOrValue and the DOM to get better offsets
	 * 
	 */
	protected int[] computeStartAndEndLocation(int startOffset, String selectionStrategy, String errorSide, String nameOrValue, IDOMDocument document) {
		try {
			int startEndPositions[] = new int[2];

			IndexedRegion region = document.getModel().getIndexedRegion(startOffset);
			IndexedRegion prevRegion = document.getModel().getIndexedRegion(startOffset - 1);

			if (prevRegion != region) {
				// if between two regions we use the 'errorSide' to understand
				// which
				// element is applicable. if we know the error has been
				// reported to the
				// right of the tag, then we can assume we need to step back
				// to the previous
				// region to land at the 'correct location. Otherwise assume
				// we're
				// exactly where we need to be.
				if (ERROR_SIDE_LEFT.equals(errorSide)) {
					region = prevRegion;
				}
			}

			// initialize start and end positions to be the start positions
			// this means if the
			// special case is not taken care of below the start and end
			// offset are set to be
			// the start of the region where the error was
			if (region != null) {
				startEndPositions[0] = region.getStartOffset();
				startEndPositions[1] = startEndPositions[0];
			}
			else { // this will message will not get added to the IReporter
				// since the length is 0
				startEndPositions[0] = 0;
				startEndPositions[1] = 0;
			}
			if (region instanceof Node) {
				Node node = (Node) region;

				if (START_TAG.equals(selectionStrategy)) {// then we want to
					// underline the opening tag
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						startEndPositions[0] = element.getStartOffset() + 1;
						startEndPositions[1] = startEndPositions[0] + element.getTagName().length();
					}
				}
				else if (END_TAG.equals(selectionStrategy)) {// then we want to
					// underline the end tag
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						startEndPositions[0] = element.getEndStartOffset();
						startEndPositions[1] = element.getEndOffset();
					}
				}
				else if (ATTRIBUTE_NAME.equals(selectionStrategy)) { // in
					// underline the attribute's name
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						IDOMNode attributeNode = (IDOMNode) (element.getAttributeNode(nameOrValue));
						if (attributeNode != null) {
							startEndPositions[0] = attributeNode.getStartOffset();
							startEndPositions[1] = attributeNode.getStartOffset() + nameOrValue.length();
						}
					}
				}
				else if (ATTRIBUTE_VALUE.equals(selectionStrategy)) {
					// underline the attribute's value
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						IDOMAttr attributeNode = (IDOMAttr) (element.getAttributeNode(nameOrValue));
						if (attributeNode != null) {
							startEndPositions[0] = attributeNode.getValueRegionStartOffset();
							String valueRegionText = attributeNode.getValueRegionText();
							int valueRegionLength = valueRegionText == null ? 0 : valueRegionText.length(); 
							startEndPositions[1] = startEndPositions[0] + valueRegionLength;
						}
					}
				}
				else if (ALL_ATTRIBUTES.equals(selectionStrategy)) {
					// underline all attributes
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						NamedNodeMap attributes = element.getAttributes();
						if (attributes != null) {
							IDOMNode first = (IDOMNode) attributes.item(0);
							IDOMNode last = (IDOMNode) attributes.item(attributes.getLength() - 1);
							if ((first != null) && (last != null)) {
								startEndPositions[0] = first.getStartOffset();
								startEndPositions[1] = last.getEndOffset();
							}
						}
					}
				}
				else if (TEXT.equals(selectionStrategy)) {
					// underline the text between the tags
					if (node.getNodeType() == Node.TEXT_NODE) {
						IDOMText textNode = (IDOMText) node;
						int start = textNode.getStartOffset();
						String value = textNode.getNodeValue();
						int index = 0;
						char curChar = value.charAt(index);
						// here we are finding start offset by skipping over
						// whitespace:
						while ((curChar == '\n') || (curChar == '\t') || (curChar == '\r') || (curChar == ' ')) {
							curChar = value.charAt(index);
							index++;
						}
						if (index > 0) {
							index--;

						}
						start = start + index;
						startEndPositions[0] = start;
						startEndPositions[1] = start + value.trim().length();
					}
					else if (node.getNodeType() == Node.ELEMENT_NODE) {
						IDOMElement element = (IDOMElement) node;
						Node child = element.getFirstChild();
						if (child instanceof IDOMNode) {
							IDOMNode xmlChild = ((IDOMNode) child);
							startEndPositions[0] = xmlChild.getStartOffset();
							startEndPositions[1] = xmlChild.getEndOffset();
						}
					}
				}
				else if (FIRST_NON_WHITESPACE_TEXT.equals(selectionStrategy)) {
					// search through all child nodes and return range of
					// first non-whitespace
					// text node
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						NodeList nodes = node.getChildNodes();
						for (int i = 0; i < nodes.getLength(); i++) {
							Node currentNode = nodes.item(i);
							if (currentNode.getNodeType() == Node.TEXT_NODE) {
								// TODO (Trung) I don't think we should call
								// getNodeValue(), trim(), length()
								// repeatedly.
								// This is inefficient, to improve use local
								// variables to store values.
								IDOMText textNode = (IDOMText) currentNode;
								if (textNode.getNodeValue().trim().length() > 0) {
									String value = textNode.getNodeValue();
									int index = 0;
									int start = textNode.getStartOffset();
									char curChar = value.charAt(index);
									// here we are finding start offset by
									// skipping over whitespace:
									while ((curChar == '\n') || (curChar == '\t') || (curChar == '\r') || (curChar == ' ')) {
										curChar = value.charAt(index);
										index++;
									}
									if (index > 0) {
										index--;

									}
									start = start + index;
									startEndPositions[0] = start;
									startEndPositions[1] = start + value.trim().length();
									break;
								}
							}

						}
					}
				}

				else if (TEXT_ENTITY_REFERENCE.equals(selectionStrategy)) {
					if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
						startEndPositions[0] = region.getStartOffset();
						startEndPositions[1] = region.getEndOffset();
					}
					else if (node.getNodeType() == Node.ELEMENT_NODE) {
						/*
						 * In this case the undeclared entity might be in one
						 * of the attribute values. Search through the
						 * attributes to find the range of the undeclared
						 * entity.
						 */
						String entity = "&" + nameOrValue + ";"; //$NON-NLS-1$ //$NON-NLS-2$
						NamedNodeMap attributes = node.getAttributes();
						for (int i = 0; i < attributes.getLength(); i++) {
							IDOMAttr attr = (IDOMAttr) attributes.item(i);
							String nodeValue = attr.getNodeValue();
							int index = nodeValue.indexOf(entity);
							if (index != -1) {
								startEndPositions[0] = attr.getValueRegionStartOffset() + index + 1;
								startEndPositions[1] = startEndPositions[0] + entity.length();
							}
						}
					}

				}
				else if (VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE.equals(selectionStrategy)) {
					// TODO (Trung) do we really need this strategy ?
					// If we know the name of the name of the attribute, we
					// can retrieve its value.
					// Hence, we can incoperate this strategy with
					// ATTRIBUTE_VALUE ?
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						// here we will search through all attributes for the
						// one with the
						// with the value we want:
						// TODO (Trung) I see a potential problem here.
						// What happens when there is another attribute having
						// the same value
						// with this attribute's buggy value ?
						// Need to solve when time permits.
						NamedNodeMap attributes = node.getAttributes();
						for (int i = 0; i < attributes.getLength(); i++) {
							IDOMAttr attr = (IDOMAttr) attributes.item(i);
							String nodeValue = attr.getNodeValue().trim();
							if (nodeValue.equals(nameOrValue)) {
								startEndPositions[0] = attr.getValueRegionStartOffset() + 1;
								startEndPositions[1] = startEndPositions[0] + nodeValue.length();
								break;
							}
						}
					}
				}
				else if (ATTRIBUTE_NAME_LAST.equals(selectionStrategy)) {
					// underline the last attribute's name
					if (node.getNodeType() == Node.ELEMENT_NODE) {
						NamedNodeMap attributeMap = node.getAttributes();
						final int length = attributeMap.getLength();
						Node tempNode = null;
						Node attrNode  = null;
						for (int i = 0; i < length; i++) {
							tempNode = attributeMap.item(i);
							if (tempNode != null && tempNode.getNodeName().equals(nameOrValue)) {
								attrNode = tempNode;
							}
						}
						IDOMNode attributeNode = (IDOMNode) (attrNode);
						if (attributeNode != null) {
							startEndPositions[0] = attributeNode.getStartOffset();
							startEndPositions[1] = attributeNode.getStartOffset() + nameOrValue.length();
						}
					}
				}
			}
			return startEndPositions;
		}
		// catch (Exception e) { // e.printStackTrace();
		// }
		finally {
		}
		// return null;
	}

	/**
	 * Returns true if delegate validator is enabled based on Validation
	 * preferences
	 * 
	 * @param file
	 * @return false if delegate validator is not enabled based on Validatoin
	 *         preferences, true otherwise
	 */
	protected boolean isDelegateValidatorEnabled(IFile file) {
		return true;
	}

	protected String getErrorSide(IMessage message) {
		// note that if the ERROR_SIDE is unspecified we return the default
		// value ERROR_SIDE_LEFT
		Object value = message.getAttribute(ERROR_SIDE);
		return ERROR_SIDE_RIGHT.equals(value) ? ERROR_SIDE_RIGHT : ERROR_SIDE_LEFT;
	}
}
