/******************************************************************************* 
 * Copyright (c) 2009-2013 Red Hat, Inc. 
 * Distributed under license by Red Hat, Inc. All rights reserved. 
 * This program is 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: 
 * Red Hat, Inc. - initial API and implementation 
 ******************************************************************************/
package org.jboss.tools.jsf.web.validation;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;

import org.apache.xerces.xni.XMLResourceIdentifier;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.apache.xerces.xni.parser.XMLParseException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.wst.validation.ValidationResult;
import org.eclipse.wst.validation.ValidationState;
import org.eclipse.wst.validation.internal.provisional.core.IMessage;
import org.eclipse.wst.xml.core.internal.validation.XMLValidationInfo;
import org.eclipse.wst.xml.core.internal.validation.core.NestedValidatorContext;
import org.eclipse.wst.xml.core.internal.validation.core.ValidationMessage;
import org.eclipse.wst.xml.core.internal.validation.core.ValidationReport;
import org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;
import org.jboss.tools.common.util.FileUtil;
import org.jboss.tools.jsf.JSFModelPlugin;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.LocatorImpl;

/**
 * @author Victor V. Rubezhny
 */
@SuppressWarnings("restriction")
public class XHTMLValidator extends Validator {
	public static final String PROBLEM_ID = JSFModelPlugin.PLUGIN_ID + "xhtmlsyntaxproblem";
	private static final String XHML_VALIDATOR_CONTEXT = "org.jboss.tools.jsf.web.validation.xhtmlValidatorContext"; //$NON-NLS-1$

	IProgressMonitor monitor;
	IResource resource;

	private String[] SAX_PARSER_FEATURES_TO_DISABLE = {
			"http://xml.org/sax/features/namespaces", 
	    	"http://xml.org/sax/features/use-entity-resolver2",
			"http://xml.org/sax/features/validation",
			"http://apache.org/xml/features/validation/dynamic",
			"http://apache.org/xml/features/validation/schema",
			"http://apache.org/xml/features/validation/schema-full-checking",
			"http://apache.org/xml/features/nonvalidating/load-external-dtd",
			"http://apache.org/xml/features/nonvalidating/load-dtd-grammar",
			"http://apache.org/xml/features/xinclude",
			"http://xml.org/sax/features/resolve-dtd-uris"
	};  

	private void setSAXParserFeatures(XMLReader reader, String[] features, boolean set) {
		for (String feature : features) {
			try {
			    reader.setFeature(feature, set);
			} catch (SAXException e) {
				JSFModelPlugin.getDefault().logError(e);
			}
		}
	}

	private void setSAXParserProperty(XMLReader reader, String property, Object value) {
		try {
    	    reader.setProperty(property, value);
		} catch (SAXException e) {
			JSFModelPlugin.getDefault().logError(e);
		}
	}

	private IDocument getDocument(IFile file) {
		if (file == null) {
			return null;
		}

		String content;
		try {
			content = FileUtil.readStream(file);
		} catch (CoreException e) {
			JSFModelPlugin.getDefault().logError(e);
			return null;
		}

		return (content == null ? null : new Document(content));
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.wst.xml.core.internal.validation.core.AbstractNestedValidator#validate(org.eclipse.core.resources.IResource, int, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public ValidationResult validate(IResource resource, int kind, ValidationState state, IProgressMonitor monitor) {
		displaySubtask(monitor, JSFValidationMessage.XHTML_VALIDATION, resource.getFullPath());
		this.resource = resource; 
		return super.validate(resource, kind, state, monitor);
	}

	@Override
	public ValidationReport validate(String uri, InputStream inputstream,
			NestedValidatorContext context) {
		displaySubtask(JSFValidationMessage.XHTML_VALIDATION, uri);
		this.resource = null; 
		return super.validate(uri, inputstream, context);
	}

	@Override
	public ValidationReport validate(String uri, InputStream inputstream,
			NestedValidatorContext context, ValidationResult result) {
		displaySubtask(JSFValidationMessage.XHTML_VALIDATION, uri);
		IDocument doc = resource instanceof IFile ? getDocument((IFile)resource) : null;
		
		XMLValidationInfo report = new XMLValidationInfo(uri);
		if (doc == null || !isXHTML(doc)) {
//			System.out.println("XHTML is NOT Detected: " + resource.getFullPath().toString());
			return report;
//		} else {
//			System.out.println("XHTML is Detected: " + resource.getFullPath().toString());
		}
		
		XHTMLElementHandler handler = new XHTMLElementHandler(uri,
				(resource instanceof IFile ? getDocument((IFile)resource) : null),
				report);

		try {
			XMLReader xmlReader = new MySAXParser();

			setSAXParserFeatures(xmlReader, SAX_PARSER_FEATURES_TO_DISABLE, false);
    	    setSAXParserProperty(xmlReader, "http://xml.org/sax/properties/lexical-handler", handler); 

			xmlReader.setProperty("http://apache.org/xml/properties/internal/entity-resolver", new NullXMLEntityResolver());

    	    xmlReader.setContentHandler(handler);
    	    xmlReader.setDTDHandler(handler);
    	    xmlReader.setErrorHandler(handler);
    	    xmlReader.setEntityResolver(handler);

			xmlReader.parse(uri);
		} catch (IOException e) {
			JSFModelPlugin.getDefault().logError(e);
		} catch (SAXNotRecognizedException e) {
			JSFModelPlugin.getDefault().logError(e);
		} catch (SAXNotSupportedException e) {
			JSFModelPlugin.getDefault().logError(e);
		} catch (SAXException e) {
			int max = handler.document.getLength();
			int currentLocation = handler.getCurrentLocation();
			int length = 0;
			if(max>0) {
				if(currentLocation+1>max) {
					currentLocation--;
				} else {
					length = 1;
				}
			}
			ElementLocation location = new ElementLocation(handler.locator.getLineNumber(), handler.locator.getColumnNumber(), currentLocation, length);
			report.addError(e.getLocalizedMessage(), handler.locator.getLineNumber(), handler.locator.getColumnNumber(), uri, null, new Object[] {location});
		}

		return report;
	}

	public boolean isXHTML(IDocument document) {
		XHTMLDetector detector = new XHTMLDetector(new StringReader(document.get()));
		return detector.detect();
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#addInfoToMessage(org.eclipse.wst.xml.core.internal.validation.core.ValidationMessage, org.eclipse.wst.validation.internal.provisional.core.IMessage)
	 */
	@Override
	protected void addInfoToMessage(ValidationMessage validationMessage, IMessage message) {
		ElementLocation location = validationMessage.getMessageArguments() == null || validationMessage.getMessageArguments().length < 1 ? 
					null: (ElementLocation)validationMessage.getMessageArguments()[0];
		if (location != null) {
			message.setLineNo(location.getLine());
			message.setOffset(location.getStart());
			message.setLength(location.getLength());
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#validationStarting(org.eclipse.core.resources.IProject, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void validationStarting(IProject project, ValidationState state,
			IProgressMonitor monitor) {
		if (project != null) {
			NestedValidatorContext context = getNestedContext(state, false);
			if (context == null) {
				context = getNestedContext(state, true);
				if (context != null)
					context.setProject(project);
				setupValidation(context);
				state.put(XHML_VALIDATOR_CONTEXT, context);
			}
			this.monitor = monitor;
			// Do not call the super.validationStarting(...) here (otherwise it
			// will break XML Validator context)
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#validationFinishing(org.eclipse.core.resources.IProject, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void validationFinishing(IProject project, ValidationState state,
			IProgressMonitor monitor) {
		if (project != null) {
			super.validationFinishing(project, state, monitor);
			NestedValidatorContext context = getNestedContext(state, false);
			if (context != null) {
				teardownValidation(context);
				state.put(XHML_VALIDATOR_CONTEXT, null);
			}
		}
	}

	private void displaySubtask(String message, Object... arguments) {
		displaySubtask(monitor, message, arguments);
	}

	private void displaySubtask(IProgressMonitor monitor, String message, Object... arguments) {
		if(monitor!=null) {
			monitor.subTask(MessageFormat.format(message, arguments));
		}
	}

	class ElementLocation {
		int line;
		int column;
		int start;
		int length;

		ElementLocation (int line, int column, int start, int length) {
			this.line = line;
			this.column = column;
			this.start = start;
			this.length = length;
		}

		int getLine() {
			return line;
		}

		int getColumn() {
			return column;
		}

		int getStart() {
			return start;
		}

		int getLength() {
			return length;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("'");
			sb.append("Line: ");
			sb.append(line);
			sb.append(", Column: ");
			sb.append(column);
			sb.append(", start: ");
			sb.append(start);
			sb.append(", end: ");
			sb.append((start + length));
			sb.append(", length: ");
			sb.append(length);
			return sb.toString();
		}
	}

	class NullXMLEntityResolver implements XMLEntityResolver {
		public XMLInputSource resolveEntity(XMLResourceIdentifier rid) throws XNIException, IOException {
			return new XMLInputSource(rid.getPublicId(), 
					rid.getBaseSystemId()==null?rid.getLiteralSystemId():rid.getExpandedSystemId(), 
							rid.getBaseSystemId(), new StringReader(""), null);
		}
	}

	class XHTMLElementHandler extends DefaultHandler implements LexicalHandler {
		private Locator locator;
		private IDocument document;

		public XHTMLElementHandler(String uri, IDocument document, XMLValidationInfo valinfo) {
			super();
			this.document = document;
		}

		@Override
		public void setDocumentLocator(Locator locator) {
			  this.locator = locator;
		}

		@Override
		public InputSource resolveEntity(String publicId, String systemId)
				throws IOException, SAXException {
			return new InputSource(new StringReader("")); //$NON-NLS-1$
		}

		private int getCurrentLocation() {
			if (locator == null) { 
				return 0;
			}

			int line = locator.getLineNumber() - 1;
			int lineOffset = locator.getColumnNumber() - 1;
			try {
				return document.getLineOffset(line) + lineOffset;
			} catch (BadLocationException e) {
				JSFModelPlugin.getDefault().logError(e);
			}
			return 0;
		}

		@Override
		public void endDTD() throws SAXException {
		}

		@Override
		public void startEntity(String name) throws SAXException {
		}

		@Override
		public void endEntity(String name) throws SAXException {
		}

		@Override
		public void startCDATA() throws SAXException {
		}

		@Override
		public void endCDATA() throws SAXException {
		}

		@Override
		public void comment(char[] ch, int start, int length) throws SAXException {
		}

		@Override
		public void startDTD(String name, String publicId, String systemId)
				throws SAXException {
		}
	}

	class MyXMLInputSource extends XMLInputSource {

		public MyXMLInputSource(String publicId, String systemId,
				String baseSystemId) {
			super(publicId, systemId, baseSystemId);
		}

		/*
		 * (non-Javadoc)
		 * @see org.apache.xerces.xni.parser.XMLInputSource#getByteStream()
		 */
		@Override
		public InputStream getByteStream() {
			InputStream stream = null;
			try {
				URL location = new URL(getSystemId());
				String protocol = location.getProtocol();
				if(!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) {
	                URLConnection connect = location.openConnection();
	                if (!(connect instanceof HttpURLConnection)) {
	                    stream = connect.getInputStream();
	                }
				}
			} catch (MalformedURLException e) {
				// Ignore (null will be returned as result)
				JSFModelPlugin.getPluginLog().logError(e);
			} catch (IOException e) {
				// Ignore (null will be returned as result)
				JSFModelPlugin.getPluginLog().logError(e);
			}
            return stream;
		}
	}

	class MySAXParser extends org.apache.xerces.parsers.SAXParser {

		/*
		 * (non-Javadoc)
		 * @see org.apache.xerces.parsers.AbstractSAXParser#parse(java.lang.String)
		 */
		@Override
		public void parse(String systemId) throws SAXException, IOException {
	        // parse document
	        XMLInputSource source = new MyXMLInputSource(null, systemId, null);
	        try {
	            parse(source);
	        }
	        // wrap XNI exceptions as SAX exceptions
	        catch (XMLParseException e) {
	            Exception ex = e.getException();
	            if (ex == null) {
	                // must be a parser exception; mine it for locator info and throw
	                // a SAXParseException
	                LocatorImpl locatorImpl = new LocatorImpl(){
	                    public String getXMLVersion() {
	                        return fVersion;
	                    }
	                    // since XMLParseExceptions know nothing about encoding,
	                    // we cannot return anything meaningful in this context.
	                    // We *could* consult the LocatorProxy, but the
	                    // application can do this itself if it wishes to possibly
	                    // be mislead.
	                    public String getEncoding() {
	                        return null;
	                    }
	                };
	                locatorImpl.setPublicId(e.getPublicId());
	                locatorImpl.setSystemId(e.getExpandedSystemId());
	                locatorImpl.setLineNumber(e.getLineNumber());
	                locatorImpl.setColumnNumber(e.getColumnNumber());
	                throw new SAXParseException(e.getMessage(), locatorImpl);
	            }
	            if (ex instanceof SAXException) {
	                // why did we create an XMLParseException?
	                throw (SAXException)ex;
	            }
	            if (ex instanceof IOException) {
	                throw (IOException)ex;
	            }
	            throw new SAXException(ex);
	        } catch (XNIException e) {
	            Exception ex = e.getException();
	            if (ex == null) {
	                throw new SAXException(e.getMessage());
	            }
	            if (ex instanceof SAXException) {
	                throw (SAXException)ex;
	            }
	            if (ex instanceof IOException) {
	                throw (IOException)ex;
	            }
	            throw new SAXException(ex);
	        }
		}

		@Override
		public void parse(InputSource inputSource) throws SAXException,
				IOException {
	        // parse document
	        try {
	            XMLInputSource xmlInputSource =
	                new XMLInputSource(inputSource.getPublicId(),
	                                   inputSource.getSystemId(),
	                                   null);
	            xmlInputSource.setByteStream(inputSource.getByteStream());
	            xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
	            xmlInputSource.setEncoding(inputSource.getEncoding());
	            parse(xmlInputSource);
	        }
	        // wrap XNI exceptions as SAX exceptions
	        catch (XMLParseException e) {
	            Exception ex = e.getException();
	            if (ex == null) {
	                // must be a parser exception; mine it for locator info and throw
	                // a SAXParseException
	                LocatorImpl locatorImpl = new LocatorImpl() {
	                    public String getXMLVersion() {
	                        return fVersion;
	                    }
	                    // since XMLParseExceptions know nothing about encoding,
	                    // we cannot return anything meaningful in this context.
	                    // We *could* consult the LocatorProxy, but the
	                    // application can do this itself if it wishes to possibly
	                    // be mislead.
	                    public String getEncoding() {
	                        return null;
	                    }
	                };
	                locatorImpl.setPublicId(e.getPublicId());
	                locatorImpl.setSystemId(e.getExpandedSystemId());
	                locatorImpl.setLineNumber(e.getLineNumber());
	                locatorImpl.setColumnNumber(e.getColumnNumber());
	                throw new SAXParseException(e.getMessage(), locatorImpl);
	            }
	            if (ex instanceof SAXException) {
	                // why did we create an XMLParseException?
	                throw (SAXException)ex;
	            }
	            if (ex instanceof IOException) {
	                throw (IOException)ex;
	            }
	            throw new SAXException(ex);
	        } catch (XNIException e) {
	            Exception ex = e.getException();
	            if (ex == null) {
	                throw new SAXException(e.getMessage());
	            }
	            if (ex instanceof SAXException) {
	                throw (SAXException)ex;
	            }
	            if (ex instanceof IOException) {
	                throw (IOException)ex;
	            }
	            throw new SAXException(ex);
	        }
		}
    }  
}