/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
*     Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.lemminx.extensions.contentmodel.participants.diagnostics;

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

import org.apache.xerces.impl.Constants;
import org.apache.xerces.impl.XMLEntityManager;
import org.apache.xerces.impl.dtd.DTDGrammar;
import org.apache.xerces.impl.dtd.XMLDTDDescription;
import org.apache.xerces.impl.dtd.XMLEntityDecl;
import org.apache.xerces.impl.validation.ValidationManager;
import org.apache.xerces.parsers.SAXParser;
import org.apache.xerces.xni.Augmentations;
import org.apache.xerces.xni.NamespaceContext;
import org.apache.xerces.xni.XMLLocator;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.grammars.XMLGrammarPool;
import org.apache.xerces.xni.parser.XMLParserConfiguration;
import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.extensions.contentmodel.participants.DTDErrorCode;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Range;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

/**
 * Extension of Xerces SAX Parser to fix some Xerces bugs:
 * 
 * <ul>
 * <li>[BUG 1]: when the DTD file path is wrong on DOCTYPE, Xerces breaks all
 * validation like syntax validation</li>
 * <li>[BUG 2]: when Xerces XML grammar pool is used, the second validation
 * ignore the existing of entities. See
 * https://github.com/redhat-developer/vscode-xml/issues/234</li>
 * </ul>
 * 
 * @author Angelo ZERR
 *
 */
public class LSPSAXParser extends SAXParser {

	private static final String DTD_NOT_FOUND = "Cannot find DTD ''{0}''.\nCreate the DTD file or configure an XML catalog for this DTD.";

	protected static final String VALIDATION_MANAGER = Constants.XERCES_PROPERTY_PREFIX
			+ Constants.VALIDATION_MANAGER_PROPERTY;

	protected static final String ENTITY_MANAGER = Constants.XERCES_PROPERTY_PREFIX + Constants.ENTITY_MANAGER_PROPERTY;

	private final DOMDocument document;

	private final LSPErrorReporterForXML reporter;

	private final XMLGrammarPool grammarPool;

	public LSPSAXParser(DOMDocument document, LSPErrorReporterForXML reporter, XMLParserConfiguration config,
			XMLGrammarPool grammarPool) {
		super(config);
		this.document = document;
		this.reporter = reporter;
		this.grammarPool = grammarPool;
		init(reporter);
	}

	private void init(LSPErrorReporterForXML reporter) {
		try {
			// Add LSP error reporter to fill LSP diagnostics from Xerces errors
			super.setProperty("http://apache.org/xml/properties/internal/error-reporter", reporter);
			super.setFeature("http://apache.org/xml/features/continue-after-fatal-error", false); //$NON-NLS-1$
			super.setFeature("http://xml.org/sax/features/namespace-prefixes", true); //$NON-NLS-1$
			super.setFeature("http://xml.org/sax/features/namespaces", true); //$NON-NLS-1$
			super.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", true);
		} catch (SAXNotRecognizedException | SAXNotSupportedException e) {
			// Should never occur.
		}
	}

	private XMLLocator locator;

	@Override
	public void startDocument(XMLLocator locator, String encoding, NamespaceContext namespaceContext,
			Augmentations augs) throws XNIException {
		this.locator = locator;
		super.startDocument(locator, encoding, namespaceContext, augs);
	}

	@Override
	public void doctypeDecl(String rootElement, String publicId, String systemId, Augmentations augs)
			throws XNIException {
		if (systemId != null) {
			// There a declared DTD in the DOCTYPE
			// <!DOCTYPE root-element SYSTEM "./extended.dtd" []>
			String eid = null;
			try {
				eid = XMLEntityManager.expandSystemId(systemId, locator.getExpandedSystemId(), false);
			} catch (java.io.IOException e) {
			}
			if (!isDTDExists(eid)) {
				// The declared DTD doesn't exist
				// <!DOCTYPE root-element SYSTEM "./dtd-doesnt-exist.dtd" []>
				try {
					// Report the error
					DOMDocumentType docType = document.getDoctype();
					Range range = new Range(document.positionAt(docType.getSystemIdNode().getStart()),
							document.positionAt(docType.getSystemIdNode().getEnd()));
					reporter.addDiagnostic(range, MessageFormat.format(DTD_NOT_FOUND, eid), DiagnosticSeverity.Error,
							DTDErrorCode.dtd_not_found.getCode());
				} catch (BadLocationException e) {
					// Do nothing
				}

				// FIX [BUG 1]
				// To avoid breaking the validation (ex : syntax validation) we mark
				// the cache DTD as true to avoid having an IOException error which breaks the
				// validation.
				// boolean readExternalSubset must be false in
				// Xerces
				// https://github.com/apache/xerces2-j/blob/e5a239b96fd2cff6566a29e7a4a3a4a2bbf9b0d4/src/org/apache/xerces/impl/XMLDocumentScannerImpl.java#L950
				ValidationManager fValidationManager = (ValidationManager) fConfiguration
						.getProperty(VALIDATION_MANAGER);
				if (fValidationManager != null) {
					fValidationManager.setCachedDTD(true);
				}
			} else {
				if (grammarPool != null) {
					// FIX [BUG 2]
					// DTD exists, get the DTD grammar from the cache
					XMLEntityManager entityManager = (XMLEntityManager) fConfiguration.getProperty(ENTITY_MANAGER);
					XMLDTDDescription grammarDesc = new XMLDTDDescription(publicId, systemId,
							locator.getExpandedSystemId(), eid, rootElement);
					DTDGrammar grammar = (DTDGrammar) grammarPool.retrieveGrammar(grammarDesc);
					if (grammar != null) {
						// The DTD grammar is in cache, we need to fill XML entity manager with the
						// entities declared in the cached DTD grammar
						fillEntities(grammar, entityManager);
					}
				}
			}
		}
		super.doctypeDecl(rootElement, publicId, systemId, augs);
	}

	private static boolean isDTDExists(String expandedSystemId) {
		if (expandedSystemId == null || expandedSystemId.isEmpty()) {
			return true;
		}
		try {
			URL location = new URL(expandedSystemId);
			URLConnection connect = location.openConnection();
			if (!(connect instanceof HttpURLConnection)) {
				InputStream stream = connect.getInputStream();
				stream.close();
			}
		} catch (Exception e) {
			return false;
		}
		return true;
	}

	/**
	 * Fill entities from the given DTD grammar to the given entity manager.
	 * 
	 * @param grammar       the DTD grammar
	 * @param entityManager the entitymanager to update with entities of the DTD
	 *                      grammar.
	 */
	private static void fillEntities(DTDGrammar grammar, XMLEntityManager entityManager) {
		int index = 0;
		XMLEntityDecl entityDecl = new XMLEntityDecl() {

			@Override
			public void setValues(String name, String publicId, String systemId, String baseSystemId, String notation,
					String value, boolean isPE, boolean inExternal) {
				if (inExternal) {
					// Only entities declared in the cached DTD grammar must be added in the XML
					// entity manager.
					entityManager.addInternalEntity(name, value);
				}
			};
		};
		while (grammar.getEntityDecl(index, entityDecl)) {
			index++;
		}
	}
}