/*******************************************************************************
 * Copyright (c) 2006, 2012 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     
 *******************************************************************************/
package org.eclipse.wst.jsdt.web.core.internal.validation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.wst.jsdt.core.compiler.IProblem;
import org.eclipse.wst.jsdt.web.core.internal.Logger;
import org.eclipse.wst.jsdt.web.core.javascript.IJsTranslation;
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslationAdapter;
import org.eclipse.wst.jsdt.web.core.javascript.JsTranslationAdapterFactory;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
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.ITextRegionCollection;
import org.eclipse.wst.validation.AbstractValidator;
import org.eclipse.wst.validation.ValidationResult;
import org.eclipse.wst.validation.ValidationState;
import org.eclipse.wst.validation.internal.core.Message;
import org.eclipse.wst.validation.internal.core.ValidationException;
import org.eclipse.wst.validation.internal.operations.IWorkbenchContext;
import org.eclipse.wst.validation.internal.provisional.core.IMessage;
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.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;

public class JsValidator extends AbstractValidator implements IValidator, IExecutableExtension {
	private static final boolean DEBUG = Boolean.valueOf(Platform.getDebugOption("org.eclipse.wst.jsdt.web.core/debug/jsvalidator")).booleanValue(); //$NON-NLS-1$
	private IValidator fMessageOriginator;
	private Set fValidFileExts = new HashSet();
	
	private static final String[] METADATA_FILES = new String[]{".settings/.jsdtscope",".settings/org.eclipse.wst.jsdt.ui.superType.container",".settings/org.eclipse.wst.jsdt.ui.superType.name"};
	private static final String JAVASCRIPT_TASK_MARKER_TYPE = "org.eclipse.wst.jsdt.core.task"; //$NON-NLS-1$
	
//	private static String [] jsdtValidator = {"org.eclipse.wst.jsdt.web.core.internal.validation.JsBatchValidator"}; //$NON-NLS-1$

	
	protected class LocalizedMessage extends Message {
		private String _message = null;
		
		public LocalizedMessage(int severity, String messageText) {
			this(severity, messageText, null);
		}
		
		public LocalizedMessage(int severity, String messageText, IResource targetObject) {
			this(severity, messageText, (Object) targetObject);
		}
		
		public LocalizedMessage(int severity, String messageText, Object targetObject) {
			super(null, severity, null);
			setLocalizedMessage(messageText);
			setTargetObject(targetObject);
		}
		
		public String getLocalizedMessage() {
			return _message;
		}
		
		
		public String getText() {
			return getLocalizedMessage();
		}
		
		
		public String getText(ClassLoader cl) {
			return getLocalizedMessage();
		}
		
		
		public String getText(Locale l) {
			return getLocalizedMessage();
		}
		
		
		public String getText(Locale l, ClassLoader cl) {
			return getLocalizedMessage();
		}
		
		public void setLocalizedMessage(String message) {
			_message = message;
		}
	}
	public JsValidator() {
		this.fMessageOriginator = this;
	}
	
	/**
	 * Creates an IMessage from an IProblem
	 * 
	 * @param problem
	 * @param f
	 * @param translation
	 * @param textDoc
	 * @return message representation of the problem, or null if it could not
	 *         create one
	 */
	private IMessage createMessageFromProblem(IProblem problem, IFile f, IJsTranslation translation, IDocument textDoc) {
		int sourceStart = problem.getSourceStart();
		int sourceEnd = problem.getSourceEnd();
		if (sourceStart == -1) {
			return null;
		}
		sourceStart = translation.getWebPageOffset(sourceStart);
		sourceEnd = translation.getWebPageOffset(sourceEnd);
		/*
		 * Bug 241794 - Validation shows errors when using JSP Expressions
		 * inside JavaScript code
		 */
		IStructuredDocument doc = (IStructuredDocument) textDoc;
		IStructuredDocumentRegion documentRegion = doc.getRegionAtCharacterOffset(sourceStart);
		if (documentRegion != null) {
			ITextRegion textRegion = documentRegion.getRegionAtCharacterOffset(sourceStart);
			/*
			 * Filter out problems from areas that aren't simple JavaScript,
			 * e.g. JSP.
			 */
			if (textRegion != null && textRegion instanceof ITextRegionCollection)
				return null;
		}

		int sev = problem.isError() ? IMessage.HIGH_SEVERITY : (problem.isWarning() ? IMessage.NORMAL_SEVERITY : IMessage.LOW_SEVERITY);
		IMessage m = new LocalizedMessage(sev, problem.getMessage(), f);
		// line numbers for marker starts @ 1
		// line numbers from document starts @ 0
		try {
			int lineNo = textDoc.getLineOfOffset(sourceStart) + 1;
			m.setLineNo(lineNo);
			m.setOffset(sourceStart);
			m.setLength(sourceEnd - sourceStart + 1);
		}
		catch (BadLocationException e) {
			Logger.logException(e);
		}
		return m;
	}
	
	void performValidation(IFile f, IReporter reporter, IStructuredModel model, boolean inBatch) {
		if (model instanceof IDOMModel) {
			IDOMModel domModel = (IDOMModel) model;
			JsTranslationAdapterFactory.setupAdapterFactory(domModel);
			IDOMDocument xmlDoc = domModel.getDocument();
			JsTranslationAdapter translationAdapter = (JsTranslationAdapter) xmlDoc.getAdapterFor(IJsTranslation.class);
			//translationAdapter.resourceChanged();
			IJsTranslation translation = translationAdapter.getJsTranslation(false);
			if (!reporter.isCancelled()) {
				translation.setProblemCollectingActive(true);
				translation.reconcileCompilationUnit();
				List problems = translation.getProblems();
				// only update task markers if the model is the same as what's on disk
				boolean updateTasks = !domModel.isDirty() && f != null && f.isAccessible();
				if (updateTasks) {
					// remove old JavaScript task markers
					try {
						IMarker[] foundMarkers = f.findMarkers(JAVASCRIPT_TASK_MARKER_TYPE, true, IResource.DEPTH_ONE);
						for (int i = 0; i < foundMarkers.length; i++) {
							foundMarkers[i].delete();
						}
					}
					catch (CoreException e) {
						Logger.logException(e);
					}
				}

//				if(!inBatch) reporter.removeAllMessages(this, f);
				// add new messages
				for (int i = 0; i < problems.size() && !reporter.isCancelled(); i++) {
					IProblem problem = (IProblem) problems.get(i);
					IMessage m = createMessageFromProblem(problem, f, translation, domModel.getStructuredDocument());
					if (m != null) {
						if (problem.getID() == IProblem.Task) {
							if (updateTasks) {
								// add new JavaScript task marker
								try {
									IMarker task = f.createMarker(JAVASCRIPT_TASK_MARKER_TYPE);
									task.setAttribute(IMarker.LINE_NUMBER, new Integer(m.getLineNumber()));
									task.setAttribute(IMarker.CHAR_START, new Integer(m.getOffset()));
									task.setAttribute(IMarker.CHAR_END, new Integer(m.getOffset() + m.getLength()));
									task.setAttribute(IMarker.MESSAGE, m.getText());
									task.setAttribute(IMarker.USER_EDITABLE, Boolean.FALSE);

									switch (m.getSeverity()) {
										case IMessage.HIGH_SEVERITY: {
											task.setAttribute(IMarker.PRIORITY, new Integer(IMarker.PRIORITY_HIGH));
											task.setAttribute(IMarker.SEVERITY, new Integer(IMarker.SEVERITY_ERROR));
										}
											break;
										case IMessage.LOW_SEVERITY : {
											task.setAttribute(IMarker.PRIORITY, new Integer(IMarker.PRIORITY_LOW));
											task.setAttribute(IMarker.SEVERITY, new Integer(IMarker.SEVERITY_INFO));
										}
											break;
										default : {
											task.setAttribute(IMarker.PRIORITY, new Integer(IMarker.PRIORITY_NORMAL));
											task.setAttribute(IMarker.SEVERITY, new Integer(IMarker.SEVERITY_WARNING));
										}
									}
								}
								catch (CoreException e) {
									Logger.logException(e);
								}
							}
						}
						else {
							reporter.addMessage(fMessageOriginator, m);
						}
					}
				}
			}
		}
	}
	
	/* Read the definition for this validator and the declared valid file extensions
	 * @see org.eclipse.core.runtime.IExecutableExtension#setInitializationData(org.eclipse.core.runtime.IConfigurationElement, java.lang.String, java.lang.Object)
	 */
	public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException {
		IConfigurationElement[] includes = config.getChildren("include"); //$NON-NLS-1$
		for (int i = 0; i < includes.length; i++) {
			IConfigurationElement[] fileexts = includes[i].getChildren("fileext"); //$NON-NLS-1$
			for (int j = 0; j < fileexts.length; j++) {
				String fileext = fileexts[j].getAttribute("ext"); //$NON-NLS-1$
				if (fileext != null) {
					fValidFileExts.add(fileext);
				}
			}
		}
	}
	
	boolean shouldValidate(IFile file) {
		IResource resource = file;
		do {
			if (resource.isDerived() || resource.isTeamPrivateMember() || !resource.isAccessible() || resource.getName().charAt(0) == '.') {
				return false;
			}
			resource = resource.getParent();
		} while ((resource.getType() & IResource.PROJECT) == 0);
		return fValidFileExts.isEmpty() || fValidFileExts.contains(file.getFileExtension());
	}
	
	public void validate(IValidationContext helper, IReporter reporter) throws ValidationException {
		/* Added by BC ---- */
		// if(true) return;
		/* end Added by BC ---- */
		
		String[] uris = helper.getURIs();
		if (uris.length > 0) {
			IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
			IFile currentFile = null;
			for (int i = 0; i < uris.length && !reporter.isCancelled(); i++) {
				currentFile = wsRoot.getFile(new Path(uris[i]));
				reporter.removeAllMessages(this, currentFile);
				if (currentFile != null && currentFile.exists()) {
					if (shouldValidate(currentFile) ){ //&& fragmentCheck(currentFile)) {
						int percent = (i * 100) / uris.length + 1;
						IMessage message = new LocalizedMessage(IMessage.LOW_SEVERITY, percent + "% " + uris[i]); //$NON-NLS-1$
						reporter.displaySubtask(this, message);
						validateFile(currentFile, reporter);
					}
					if (DEBUG) {
						System.out.println("validating: [" + uris[i] + "]"); //$NON-NLS-1$ //$NON-NLS-2$
					}
				}
			}
		} else {
			// if uris[] length 0 -> validate() gets called for each project
			if (helper instanceof IWorkbenchContext) {
				IProject project = ((IWorkbenchContext) helper).getProject();
				JSFileVisitor visitor = new JSFileVisitor(reporter);
				try {
					// collect all jsp files for the project
					project.accept(visitor, IResource.DEPTH_INFINITE);
				} catch (CoreException e) {
					if (DEBUG) {
						e.printStackTrace();
					}
				}
				IFile[] files = visitor.getFiles();
				for (int i = 0; i < files.length && !reporter.isCancelled(); i++) {
					int percent = (i * 100) / files.length + 1;
					IMessage message = new LocalizedMessage(IMessage.LOW_SEVERITY, percent + "% " + files[i].getFullPath().toString()); //$NON-NLS-1$
					reporter.displaySubtask(this, message);
					validateFile(files[i], reporter);
					if (DEBUG) {
						System.out.println("validating: [" + files[i] + "]"); //$NON-NLS-1$ //$NON-NLS-2$
					}
				}
			}
		}
	}

	protected class JSFileVisitor implements IResourceProxyVisitor {
		private List fFiles = new ArrayList();
		private IReporter fReporter = null;
		
		public JSFileVisitor(IReporter reporter) {
			fReporter = reporter;
		}
		
		public final IFile[] getFiles() {
			return (IFile[]) fFiles.toArray(new IFile[fFiles.size()]);
		}
		
		
		public boolean visit(IResourceProxy proxy) throws CoreException {
			// check validation
			if (fReporter.isCancelled()) {
				return false;
			}
			if (proxy.getType() == IResource.FILE) {
				if (Util.isJsType(proxy.getName())) {
					IFile file = (IFile) proxy.requestResource();
					if (file.exists() && shouldValidate(file)) {
						if (DEBUG) {
							System.out.println("(+) JSPValidator adding file: " + file.getName()); //$NON-NLS-1$
						}
						fFiles.add(file);
						// don't search deeper for files
						return false;
					}
				}
			}
			return true;
		}
	}
	public void cleanup(IReporter reporter) {
		// nothing to do
	}
	/**
	 * Validate one file. It's assumed that the file has JSP content type.
	 * 
	 * @param f
	 * @param reporter
	 */
	
	protected void validateFile(IFile f, IReporter reporter) {
		if (JsValidator.DEBUG) {
			Logger.log(Logger.INFO, getClass().getName() + " validating: " + f); //$NON-NLS-1$
		}
		IStructuredModel model = null;
		try {
			// get jsp model, get tranlsation
			model = StructuredModelManager.getModelManager().getModelForRead(f);
			if (!reporter.isCancelled() && model != null) {
				// get DOM model then translation
				//WorkbenchReporter.removeAllMessages(f.getProject(), jsdtValidator, f.toString());
				//reporter.removeAllMessages(fMessageOriginator, f);
				performValidation(f, reporter, model, false);
			}
		} catch (IOException e) {
			Logger.logException(e);
		} catch (CoreException e) {
			Logger.logException(e);
		} finally {
			if (model != null) {
				model.releaseFromRead();
			}
		}
	}
	public ValidationResult validate(IResource resource, int kind, ValidationState state, IProgressMonitor monitor) {
		if (resource.getType() != IResource.FILE || !shouldValidate((IFile) resource))
			return null;
		ValidationResult result = new ValidationResult();
		IReporter reporter = result.getReporter(monitor);
		IFile file = (IFile) resource;
		validateFile(file, reporter);
		result.setDependsOn(createDependencies(file));
		return result;
	}

	private IResource[] createDependencies(IFile file) {
		IFile[] depends = new IFile[METADATA_FILES.length];
		for (int i = 0; i < METADATA_FILES.length; i++) {
			depends[i] = file.getProject().getFile(METADATA_FILES[i]);
		}
		return depends;
	}
}
