/*******************************************************************************
 * Copyright (c) 2005, 2008 IBM Corporation and others.
 *
 * 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.ltk.internal.core.refactoring;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
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.helpers.DefaultHandler;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.ltk.core.refactoring.IRefactoringCoreStatusCodes;
import org.eclipse.ltk.core.refactoring.RefactoringContribution;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringSessionDescriptor;
import org.eclipse.ltk.internal.core.refactoring.history.DefaultRefactoringDescriptor;
import org.eclipse.ltk.internal.core.refactoring.history.RefactoringContributionManager;

/**
 * Refactoring session reader for XML-based refactoring sessions.
 *
 * @since 3.2
 */
public final class RefactoringSessionReader extends DefaultHandler {

	/** The comment of the refactoring session, or <code>null</code> */
	private String fComment= null;

	/**
	 * The project of the refactoring descriptors, or <code>null</code>
	 * if the project should be read from the descriptors.
	 */
	private final String fProject;

	private final boolean fCreateDefaultDescriptors;

	/**
	 * The current list of refactoring descriptors, or <code>null</code>
	 * (element type: <code>RefactoringDescriptor</code>)
	 */
	private List<RefactoringDescriptor> fRefactoringDescriptors= null;

	/** Has a session been found during parsing? */
	private boolean fSessionFound= false;

	/** The current version of the refactoring script, or <code>null</code> */
	private String fVersion= null;

	private Locator fLocator;

	/**
	 * Creates a new refactoring session reader.
	 *
	 * @param createDefaultDescriptors
	 *            <code>true</code> iff {@link DefaultRefactoringDescriptor}s should be created,
	 *            <code>false</code> if {@link RefactoringContribution#createDescriptor(String, String, String, String, Map, int)}
	 *            should be used to create contribution-specific descriptors.
	 *            Note that default descriptors may miss the project property and that they
	 *            are <b>not</b> capable of creating a refactoring.
	 * @param project
	 *            the project of the refactoring descriptors, or <code>null</code>
	 *            if the project should be read from the descriptors. This parameter is not used
	 *            if <code>createDefaultDescriptors</code> is <code>true</code>.
	 */
	public RefactoringSessionReader(boolean createDefaultDescriptors, String project) {
		fCreateDefaultDescriptors= createDefaultDescriptors;
		fProject= project;
	}

	/**
	 * Creates a new parser from the specified factory.
	 *
	 * @param factory
	 *            the parser factoring to use
	 * @return the created parser
	 * @throws ParserConfigurationException
	 *             if no parser is available with the given configuration
	 * @throws SAXException
	 *             if an error occurs while creating the parser
	 */
	private SAXParser createParser(final SAXParserFactory factory) throws ParserConfigurationException, SAXException {

		final SAXParser parser= factory.newSAXParser();
		final XMLReader reader= parser.getXMLReader();

		try {

			reader.setFeature("http://xml.org/sax/features/validation", false); //$NON-NLS-1$
			reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); //$NON-NLS-1$

		} catch (SAXNotRecognizedException | SAXNotSupportedException exception) {
			// Do nothing
		}
		return parser;
	}

	/**
	 * Reads a refactoring history descriptor from the specified input object.
	 *
	 * @param source
	 *            the input source
	 * @return a corresponding refactoring history descriptor, or
	 *         <code>null</code>
	 * @throws CoreException
	 *             if an error occurs while reading form the input source
	 */
	public RefactoringSessionDescriptor readSession(final InputSource source) throws CoreException {
		fSessionFound= false;
		try {
			source.setSystemId("/"); //$NON-NLS-1$
			createParser(SAXParserFactory.newInstance()).parse(source, this);
			if (!fSessionFound)
				throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), IRefactoringCoreStatusCodes.REFACTORING_HISTORY_FORMAT_ERROR, RefactoringCoreMessages.RefactoringSessionReader_no_session, null));
			if (fRefactoringDescriptors != null) {
				if (fVersion == null || "".equals(fVersion)) //$NON-NLS-1$
					throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), IRefactoringCoreStatusCodes.MISSING_REFACTORING_HISTORY_VERSION, RefactoringCoreMessages.RefactoringSessionReader_missing_version_information, null));
				if (!IRefactoringSerializationConstants.CURRENT_VERSION.equals(fVersion))
					throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), IRefactoringCoreStatusCodes.UNSUPPORTED_REFACTORING_HISTORY_VERSION, RefactoringCoreMessages.RefactoringSessionReader_unsupported_version_information, null));
				return new RefactoringSessionDescriptor(fRefactoringDescriptors.toArray(new RefactoringDescriptor[fRefactoringDescriptors.size()]), fVersion, fComment);
			}
		} catch (SAXParseException exception) {
			String message= Messages.format(RefactoringCoreMessages.RefactoringSessionReader_invalid_contents_at,
					new Object[] {
							Integer.toString(exception.getLineNumber()),
							Integer.toString(exception.getColumnNumber())
			});
			throwCoreException(exception, message);
		} catch (IOException | ParserConfigurationException | SAXException exception) {
			throwCoreException(exception, exception.getLocalizedMessage());
		} finally {
			fRefactoringDescriptors= null;
			fVersion= null;
			fComment= null;
			fLocator= null;
		}
		return null;
	}

	private void throwCoreException(Exception exception, String message) throws CoreException {
		throw new CoreException(new Status(IStatus.ERROR,
				RefactoringCorePlugin.getPluginId(),
				IRefactoringCoreStatusCodes.REFACTORING_HISTORY_IO_ERROR,
				message,
				exception));
	}

	/*
	 * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator)
	 */
	@Override
	public void setDocumentLocator(Locator locator) {
		fLocator= locator;
	}

	@Override
	public void startElement(final String uri, final String localName, final String qualifiedName, final Attributes attributes) throws SAXException {
		if (IRefactoringSerializationConstants.ELEMENT_REFACTORING.equals(qualifiedName)) {
			final int length= attributes.getLength();
			final Map<String, String> map= new HashMap<>(length);
			String id= ""; //$NON-NLS-1$
			String stamp= ""; //$NON-NLS-1$
			String description= ""; //$NON-NLS-1$
			String comment= null;
			String flags= "0"; //$NON-NLS-1$
			String project= null;
			for (int index= 0; index < length; index++) {
				final String name= attributes.getQName(index);
				final String value= attributes.getValue(index);
				if (IRefactoringSerializationConstants.ATTRIBUTE_ID.equals(name)) {
					id= value;
				} else if (IRefactoringSerializationConstants.ATTRIBUTE_STAMP.equals(name)) {
					stamp= value;
				} else if (IRefactoringSerializationConstants.ATTRIBUTE_DESCRIPTION.equals(name)) {
					description= value;
				} else if (IRefactoringSerializationConstants.ATTRIBUTE_FLAGS.equals(name)) {
					flags= value;
				} else if (IRefactoringSerializationConstants.ATTRIBUTE_COMMENT.equals(name)) {
					if (!"".equals(value)) //$NON-NLS-1$
						comment= value;
				} else if (IRefactoringSerializationConstants.ATTRIBUTE_PROJECT.equals(name)) {
					project= value;
				} else if (!"".equals(name)) { //$NON-NLS-1$
					map.put(name, value);
				}
			}
			int flag= 0;
			try {
				flag= Integer.parseInt(flags);
			} catch (NumberFormatException exception) {
				// Do nothing
			}

			RefactoringDescriptor descriptor= null;
			if (fCreateDefaultDescriptors) {
				descriptor= new DefaultRefactoringDescriptor(id, project, description, comment, map, flag);
			} else {
				if (fProject != null && project == null) {
					project= fProject; // override project from file if fProject != null
				}
				try {
					descriptor= RefactoringContributionManager.getInstance().createDescriptor(id, project, description, comment, map, flag);
				} catch (RuntimeException e) {
					throw new SAXParseException(RefactoringCoreMessages.RefactoringSessionReader_invalid_values_in_xml, fLocator, e) {
						private static final long serialVersionUID= 1L;
						@Override
						public Throwable getCause() { // support proper 1.4-style exception chaining
							return getException();
						}
					};
				}
			}
			try {
				descriptor.setTimeStamp(Long.valueOf(stamp).longValue());
			} catch (NumberFormatException exception) {
				// Do nothing
			}
			if (fRefactoringDescriptors == null)
				fRefactoringDescriptors= new ArrayList<>();
			fRefactoringDescriptors.add(descriptor);

		} else if (IRefactoringSerializationConstants.ELEMENT_SESSION.equals(qualifiedName)) {
			fSessionFound= true;
			final String version= attributes.getValue(IRefactoringSerializationConstants.ATTRIBUTE_VERSION);
			if (version != null && !"".equals(version)) //$NON-NLS-1$
				fVersion= version;
			fComment= attributes.getValue(IRefactoringSerializationConstants.ATTRIBUTE_COMMENT);
		}
	}
}
