/*******************************************************************************
 * Copyright (c) 2005, 2015 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.history;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.ltk.core.refactoring.IRefactoringCoreStatusCodes;
import org.eclipse.ltk.core.refactoring.RefactoringContribution;
import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptorProxy;
import org.eclipse.ltk.core.refactoring.RefactoringSessionDescriptor;
import org.eclipse.ltk.core.refactoring.history.RefactoringHistory;
import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
import org.eclipse.ltk.internal.core.refactoring.IRefactoringSerializationConstants;
import org.eclipse.ltk.internal.core.refactoring.Messages;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin;
import org.eclipse.ltk.internal.core.refactoring.RefactoringSessionReader;
import org.eclipse.ltk.internal.core.refactoring.RefactoringSessionTransformer;

/**
 * Manager for persistable refactoring histories.
 *
 * @since 3.2
 */
public final class RefactoringHistoryManager {

	/** The index component delimiter */
	public static final char DELIMITER_COMPONENT= '\t';

	/** The index entry delimiter */
	public static final char DELIMITER_ENTRY= '\n';

	/** The calendar instance */
	private static final Calendar fgCalendar= Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); //$NON-NLS-1$

	/**
	 * Checks whether the argument map is well-formed.
	 * <p>
	 * All arguments contained in the map are checked according to the rules of
	 * {@link RefactoringDescriptor}.
	 * </p>
	 *
	 * @param arguments
	 *            the argument map
	 * @throws CoreException
	 *             if the argument violates any of the constraints
	 */
	public static void checkArgumentMap(final Map<String, String> arguments) throws CoreException {
		Assert.isNotNull(arguments);
		for (Entry<?, ?> entry : arguments.entrySet()) {
			if (entry.getKey() instanceof String) {
				final String string= (String) entry.getKey();
				final char[] characters= string.toCharArray();
				if (characters.length == 0) {
					throw new CoreException(new Status(IStatus.ERROR, RefactoringCore.ID_PLUGIN, IRefactoringCoreStatusCodes.REFACTORING_HISTORY_FORMAT_ERROR, RefactoringCoreMessages.RefactoringHistoryManager_empty_argument, null));
				}
				for (char character : characters) {
					if (Character.isWhitespace(character))
						throw new CoreException(new Status(IStatus.ERROR, RefactoringCore.ID_PLUGIN, IRefactoringCoreStatusCodes.REFACTORING_HISTORY_FORMAT_ERROR, RefactoringCoreMessages.RefactoringHistoryManager_whitespace_argument_key, null));
				}
			} else {
				throw new CoreException(new Status(IStatus.ERROR, RefactoringCore.ID_PLUGIN, IRefactoringCoreStatusCodes.REFACTORING_HISTORY_FORMAT_ERROR, Messages.format(RefactoringCoreMessages.RefactoringHistoryManager_non_string_argument, entry.getKey()), null));
			}
			if (! (entry.getValue() instanceof String)) {
				throw new CoreException(new Status(IStatus.ERROR, RefactoringCore.ID_PLUGIN, IRefactoringCoreStatusCodes.REFACTORING_HISTORY_FORMAT_ERROR, Messages.format(RefactoringCoreMessages.RefactoringHistoryManager_non_string_value, entry.getKey()), null));
			}
		}
	}

	/**
	 * Creates a new core exception representing an I/O error.
	 *
	 * @param exception
	 *            the throwable to wrap
	 * @return the core exception
	 */
	private static CoreException createCoreException(final Throwable exception) {
		return new CoreException(new Status(IStatus.ERROR, RefactoringCore.ID_PLUGIN, IRefactoringCoreStatusCodes.REFACTORING_HISTORY_IO_ERROR, exception.getLocalizedMessage(), exception));
	}

	/**
	 * Escapes the specified string for the history index.
	 *
	 * @param string
	 *            the string for the history index
	 * @return the escaped string
	 */
	public static String escapeString(final String string) {
		if (string.indexOf(DELIMITER_COMPONENT) < 0) {
			final int length= string.length();
			final StringBuilder buffer= new StringBuilder(length + 16);
			for (int index= 0; index < length; index++) {
				final char character= string.charAt(index);
				if (DELIMITER_COMPONENT == character)
					buffer.append(DELIMITER_COMPONENT);
				buffer.append(character);
			}
			return buffer.toString();
		}
		return string;
	}

	/**
	 * Returns the argument map of the specified descriptor.
	 *
	 * @param descriptor
	 *            the refactoring descriptor
	 * @return the argument map, or <code>null</code>
	 */
	public static Map<String, String> getArgumentMap(final RefactoringDescriptor descriptor) {
		Map<String, String> arguments= null;
		final RefactoringContribution contribution= RefactoringContributionManager.getInstance().getRefactoringContribution(descriptor.getID());
		if (contribution != null)
			arguments= contribution.retrieveArgumentMap(descriptor);
		else if (descriptor instanceof DefaultRefactoringDescriptor)
			arguments= ((DefaultRefactoringDescriptor) descriptor).getArguments();
		return arguments;
	}

	/**
	 * Reads refactoring descriptor proxies.
	 *
	 * @param store
	 *            the file store to read
	 * @param project
	 *            the name of the project, or <code>null</code> for the
	 *            workspace
	 * @param collection
	 *            the collection of proxies to fill in
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp, inclusive
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label to use
	 * @throws CoreException
	 *             if an error occurs
	 */
	private static void readRefactoringDescriptorProxies(final IFileStore store, final String project, final Collection<RefactoringDescriptorProxy> collection, final long start, final long end, final IProgressMonitor monitor, final String task) throws CoreException {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_retrieving_history, 22);
			final IFileInfo info= store.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 2, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			if (!info.isDirectory() && info.exists() && RefactoringHistoryService.NAME_INDEX_FILE.equalsIgnoreCase(store.getName())) {
				try (InputStream stream= store.openInputStream(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL))) {
					final RefactoringDescriptorProxy[] proxies= readRefactoringDescriptorProxies(stream, project, start, end);
					collection.addAll(Arrays.asList(proxies));
					monitor.worked(1);
				} catch (IOException exception) {
					throw createCoreException(exception);
				} finally {
					monitor.worked(1);
				}
			} else
				monitor.worked(4);
			if (monitor.isCanceled())
				throw new OperationCanceledException();
			final IFileStore[] stores= store.childStores(EFS.NONE, new SubProgressMonitor(monitor, 2, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			final IProgressMonitor subMonitor= new SubProgressMonitor(monitor, 12);
			try {
				subMonitor.beginTask(task, stores.length);
				for (IFileStore s : stores) {
					readRefactoringDescriptorProxies(s, project, collection, start, end, new SubProgressMonitor(subMonitor, 1), task);
				}
			} finally {
				subMonitor.done();
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 * Reads refactoring descriptor proxies from the specified input stream.
	 * <p>
	 * The refactoring descriptor proxies are returned in no particular order.
	 * </p>
	 *
	 * @param stream
	 *            the input stream where to read from
	 * @param project
	 *            the name of the project, or <code>null</code> for the
	 *            workspace
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp, inclusive
	 * @return An array of refactoring descriptor proxies
	 * @throws IOException
	 *             if an input/output error occurs
	 */
	public static RefactoringDescriptorProxy[] readRefactoringDescriptorProxies(final InputStream stream, final String project, final long start, final long end) throws IOException {
		final List<DefaultRefactoringDescriptorProxy> list= new ArrayList<>();
		final BufferedReader reader= new BufferedReader(new InputStreamReader(stream, IRefactoringSerializationConstants.OUTPUT_ENCODING));
		while (reader.ready()) {
			final String line= reader.readLine();
			if (line != null) {
				final int index= line.indexOf(DELIMITER_COMPONENT);
				if (index > 0) {
					try {
						final long stamp= Long.parseLong(line.substring(0, index));
						if (stamp >= start && stamp <= end)
							list.add(new DefaultRefactoringDescriptorProxy(unescapeString(line.substring(index + 1)), project, stamp));
					} catch (NumberFormatException exception) {
						// Just skip
					}
				}
			}
		}
		return list.toArray(new RefactoringDescriptorProxy[list.size()]);
	}

	/**
	 * Reads default refactoring descriptors from the specified input stream.
	 * <p>
	 * The refactoring descriptors are returned in no particular order.
	 * </p>
	 *
	 * @param stream
	 *            the input stream where to read from
	 * @return An array of refactoring descriptor proxies
	 * @throws CoreException
	 *             if an error occurs while reading the descriptors
	 */
	public static DefaultRefactoringDescriptor[] readRefactoringDescriptors(final InputStream stream) throws CoreException {
		final List<RefactoringDescriptor> list= new ArrayList<>(64);
		readRefactoringDescriptors(stream, list, new NullProgressMonitor());
		return list.toArray(new DefaultRefactoringDescriptor[list.size()]);
	}

	/**
	 * Reads default refactoring descriptors from the specified input stream.
	 *
	 * @param stream
	 *            the input stream where to read from
	 * @param collection
	 *            the list of descriptors read from the history
	 * @param monitor
	 *            the progress monitor to use
	 * @throws CoreException
	 *             if an error occurs while reading the descriptors
	 */
	private static void readRefactoringDescriptors(final InputStream stream, final Collection<RefactoringDescriptor> collection, final IProgressMonitor monitor) throws CoreException {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_retrieving_history, 1);
			final RefactoringDescriptor[] results= new RefactoringSessionReader(true, null).readSession(new InputSource(new BufferedInputStream(stream))).getRefactorings();
			Collections.addAll(collection, results);
		} finally {
			monitor.done();
		}
	}

	/**
	 * Removes the refactoring history index tree spanned by the specified file
	 * store.
	 *
	 * @param store
	 *            the file store spanning the history index tree
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label to use
	 * @throws CoreException
	 *             if an error occurs while removing the index tree
	 */
	private static void removeIndexTree(final IFileStore store, final IProgressMonitor monitor, final String task) throws CoreException {
		try {
			monitor.beginTask(task, 16);
			final IFileInfo info= store.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			if (info.isDirectory()) {
				if (RefactoringHistoryService.NAME_HISTORY_FOLDER.equalsIgnoreCase(info.getName()))
					return;
				final IFileStore[] stores= store.childStores(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
				final IProgressMonitor subMonitor= new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL);
				try {
					subMonitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_updating_history, stores.length);
					for (IFileStore s : stores) {
						final IFileInfo current= s.fetchInfo(EFS.NONE, new SubProgressMonitor(subMonitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
						if (current.isDirectory()) {
							for (char character : s.getName().toCharArray()) {
								if (Character.isDigit(character))
									return;
							}
						}
					}
				} finally {
					subMonitor.done();
				}
			}
			final IFileStore parent= store.getParent();
			store.delete(0, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			removeIndexTree(parent, new SubProgressMonitor(monitor, 12, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), task);
		} finally {
			monitor.done();
		}
	}

	/**
	 * Sorts the refactoring descriptor proxies in ascending order of their time
	 * stamps.
	 *
	 * @param descriptors
	 *            the refactoring descriptors
	 */
	public static void sortRefactoringDescriptorsAscending(final RefactoringDescriptor[] descriptors) {
		Arrays.sort(descriptors, Comparator.comparing(RefactoringDescriptor::getTimeStamp));
	}

	/**
	 * Sorts the refactoring descriptor proxies in ascending order of their time
	 * stamps.
	 *
	 * @param proxies
	 *            the refactoring descriptor proxies
	 */
	public static void sortRefactoringDescriptorsAscending(final RefactoringDescriptorProxy[] proxies) {
		Arrays.sort(proxies, Comparator.comparing(RefactoringDescriptorProxy::getTimeStamp));
	}

	/**
	 * Sorts the refactoring descriptor proxies in descending order of their
	 * time stamps.
	 *
	 * @param proxies
	 *            the refactoring descriptor proxies
	 */
	public static void sortRefactoringDescriptorsDescending(final RefactoringDescriptorProxy[] proxies) {
		Arrays.sort(proxies, Comparator.comparing(RefactoringDescriptorProxy::getTimeStamp).reversed());
	}

	/**
	 * Returns a path representing the history part for the specified time
	 * stamp.
	 *
	 * @param stamp
	 *            the time stamp
	 * @return A path representing the folder of the history part
	 */
	public static IPath stampToPath(final long stamp) {
		fgCalendar.setTimeInMillis(stamp);
		final StringBuilder buffer= new StringBuilder(256);
		buffer.append(fgCalendar.get(Calendar.YEAR));
		buffer.append(IPath.SEPARATOR);
		buffer.append(fgCalendar.get(Calendar.MONTH) + 1);
		buffer.append(IPath.SEPARATOR);
		buffer.append(fgCalendar.get(Calendar.WEEK_OF_YEAR));
		return new Path(buffer.toString());
	}

	/**
	 * Transforms the specified refactoring descriptor into a DOM node.
	 *
	 * @param descriptor
	 *            the descriptor to transform
	 * @param projects
	 *            <code>true</code> to include project information,
	 *            <code>false</code> otherwise
	 * @return the DOM node representing the refactoring descriptor
	 * @throws CoreException
	 *             if an error occurs while transforming the descriptor
	 */
	private static Document transformDescriptor(final RefactoringDescriptor descriptor, final boolean projects) throws CoreException {
		final RefactoringSessionTransformer transformer= new RefactoringSessionTransformer(projects);
		try {
			transformer.beginSession(null, IRefactoringSerializationConstants.CURRENT_VERSION);
			try {
				final String id= descriptor.getID();
				transformer.beginRefactoring(id, descriptor.getTimeStamp(), descriptor.getProject(), descriptor.getDescription(), descriptor.getComment(), descriptor.getFlags());
				final Map<String, String> arguments= getArgumentMap(descriptor);
				if (arguments != null) {
					checkArgumentMap(arguments);
					for (Entry<String, String> entry : arguments.entrySet()) {
						transformer.createArgument(entry.getKey(), entry.getValue());
					}
				}
			} finally {
				transformer.endRefactoring();
			}
		} finally {
			transformer.endSession();
		}
		return transformer.getResult();
	}

	/**
	 * Un-escapes the specified string from the history index.
	 *
	 * @param string
	 *            the string from the history index
	 * @return the un-escaped string
	 */
	public static String unescapeString(final String string) {
		if (string.indexOf(DELIMITER_COMPONENT) < 0) {
			final int length= string.length();
			final StringBuilder buffer= new StringBuilder(length);
			for (int index= 0; index < length; index++) {
				final char character= string.charAt(index);
				if (DELIMITER_COMPONENT == character) {
					if (index < length - 1) {
						final char escape= string.charAt(index + 1);
						if (DELIMITER_COMPONENT == escape)
							continue;
					}
				}
				buffer.append(character);
			}
			return buffer.toString();
		}
		return string;
	}

	/**
	 * Writes the specified index entry to the refactoring history.
	 *
	 * @param file
	 *            the history index file
	 * @param proxies
	 *            the refactoring descriptors
	 * @param flags
	 *            the flags to use (either {@link EFS#NONE} or
	 *            {@link EFS#APPEND})
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label
	 * @throws CoreException
	 *             if an error occurs while writing the index entry
	 * @throws IOException
	 *             if an input/output error occurs
	 */
	private static void writeIndexEntry(final IFileStore file, final RefactoringDescriptorProxy[] proxies, final int flags, final IProgressMonitor monitor, final String task) throws CoreException, IOException {
		OutputStream output= null;
		try {
			monitor.beginTask(task, 2);
			file.getParent().mkdir(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			output= new BufferedOutputStream(file.openOutputStream(flags, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
			writeRefactoringDescriptorProxies(output, proxies);
		} finally {
			monitor.done();
			if (output != null) {
				try {
					output.close();
				} catch (IOException exception) {
					// Do nothing
				}
			}
		}
	}

	/**
	 * Writes refactoring descriptor proxies to the specified output stream.
	 *
	 * @param stream
	 *            the output stream where to write to
	 * @param proxies
	 *            the refactoring descriptor proxies to write
	 * @throws IOException
	 *             if an input/output error occurs
	 */
	public static void writeRefactoringDescriptorProxies(final OutputStream stream, final RefactoringDescriptorProxy[] proxies) throws IOException {
		final StringBuilder buffer= new StringBuilder(proxies.length * 64);
		sortRefactoringDescriptorsAscending(proxies);
		for (RefactoringDescriptorProxy proxy : proxies) {
			buffer.append(proxy.getTimeStamp());
			buffer.append(DELIMITER_COMPONENT);
			buffer.append(escapeString(proxy.getDescription()));
			buffer.append(DELIMITER_ENTRY);
		}
		stream.write(buffer.toString().getBytes(IRefactoringSerializationConstants.OUTPUT_ENCODING));
	}

	/**
	 * A simple XML writer.  Using this instead of the javax.xml.transform classes allows
	 * compilation against JCL Foundation (bug 80053).
	 */
	private static final class DOMWriter extends PrintWriter {

		/* constants */
		private static final String XML_VERSION= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$

		/**
		 * Creates a new DOM writer on the given output writer.
		 *
		 * @param output the output writer
		 */
		public DOMWriter(Writer output) {
			super(output);
		}

		public void printDocument(Document doc) {
			println(XML_VERSION);
			printElement(doc.getDocumentElement());
		}

		/**
		 * Prints the given element.
		 *
		 * @param element the element to print
		 */
		public void printElement(Element element) {
			// Ensure extra whitespace is not emitted next to a Text node,
			// as that will result in a situation where the restored text data is not the
			// same as the saved text data.
			boolean hasChildren= element.hasChildNodes();
			startTag(element, hasChildren);
			if (hasChildren) {
				boolean prevWasText= false;
				NodeList children= element.getChildNodes();
				for (int i= 0; i < children.getLength(); i++) {
					Node node= children.item(i);
					if (node instanceof Element) {
						if (!prevWasText) {
							println();
						}
						printElement((Element) children.item(i));
						prevWasText= false;
					} else if (node instanceof Text) {
						print(getEscaped(node.getNodeValue()));
						prevWasText= true;
					}
				}
				if (!prevWasText) {
					println();
				}
				endTag(element);
			}
		}

		private void startTag(Element element, boolean hasChildren) {
			StringBuilder sb= new StringBuilder();
			sb.append("<"); //$NON-NLS-1$
			sb.append(element.getTagName());
			NamedNodeMap attributes= element.getAttributes();
			for (int i= 0; i < attributes.getLength(); i++) {
				Attr attribute= (Attr) attributes.item(i);
				sb.append(" "); //$NON-NLS-1$
				sb.append(attribute.getName());
				sb.append("=\""); //$NON-NLS-1$
				sb.append(getEscaped(String.valueOf(attribute.getValue())));
				sb.append("\""); //$NON-NLS-1$
			}
			sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$
			print(sb.toString());
		}

		private void endTag(Element element) {
			StringBuilder sb= new StringBuilder();
			sb.append("</"); //$NON-NLS-1$
			sb.append(element.getNodeName());
			sb.append(">"); //$NON-NLS-1$
			print(sb.toString());
	}

		private static void appendEscapedChar(StringBuilder buffer, char c) {
			String replacement= getReplacement(c);
			if (replacement != null) {
				buffer.append('&');
				buffer.append(replacement);
				buffer.append(';');
			} else {
				buffer.append(c);
			}
		}

		private static String getEscaped(String s) {
			StringBuilder result= new StringBuilder(s.length() + 10);
			for (char c : s.toCharArray()) {
				appendEscapedChar(result, c);
			}
			return result.toString();
		}

		private static String getReplacement(char c) {
			// Encode special XML characters into the equivalent character references.
			// The first five are defined by default for all XML documents.
			// The next three (#xD, #xA, #x9) are encoded to avoid them
			// being converted to spaces on deserialization
			// (fixes bug 93720)
			switch (c) {
				case '<':
					return "lt"; //$NON-NLS-1$
				case '>':
					return "gt"; //$NON-NLS-1$
				case '"':
					return "quot"; //$NON-NLS-1$
				case '\'':
					return "apos"; //$NON-NLS-1$
				case '&':
					return "amp"; //$NON-NLS-1$
				case '\r':
					return "#x0D"; //$NON-NLS-1$
				case '\n':
					return "#x0A"; //$NON-NLS-1$
				case '\u0009':
					return "#x09"; //$NON-NLS-1$
			}
			return null;
		}
	}

	/**
	 * Writes refactoring session descriptor to the specified output stream.
	 *
	 * @param stream
	 *            the output stream where to write to
	 * @param descriptor
	 *            the refactoring session descriptors to write
	 * @param stamps
	 *            <code>true</code> to write time stamps as well,
	 *            <code>false</code> otherwise
	 * @throws CoreException
	 *             if an error occurs while writing the refactoring session
	 *             descriptor
	 */
	public static void writeRefactoringSession(final OutputStream stream, final RefactoringSessionDescriptor descriptor, final boolean stamps) throws CoreException {
		final RefactoringSessionTransformer transformer= new RefactoringSessionTransformer(true);
		final RefactoringDescriptor[] descriptors= descriptor.getRefactorings();
		try {
			transformer.beginSession(descriptor.getComment(), descriptor.getVersion());
			for (RefactoringDescriptor current : descriptors) {
				if (current != null) {
					try {
						long stamp= stamps ? current.getTimeStamp() : -1;
						transformer.beginRefactoring(current.getID(), stamp, current.getProject(), current.getDescription(), current.getComment(), current.getFlags());
						final Map<String, String> arguments= getArgumentMap(current);
						if (arguments != null) {
							checkArgumentMap(arguments);
							for (Entry<String, String> entry : arguments.entrySet()) {
								transformer.createArgument(entry.getKey(), entry.getValue());
							}
						}
					} finally {
						transformer.endRefactoring();
					}
				}
			}
		} finally {
			transformer.endSession();
		}
		final Document result= transformer.getResult();
		writeNode(stream, result);
			}

	private static void writeNode(final OutputStream stream, Document document) {
		OutputStreamWriter outputStreamWriter= new OutputStreamWriter(stream, Charset.forName("UTF-8")); //$NON-NLS-1$
		@SuppressWarnings("resource")
		DOMWriter writer= new DOMWriter(outputStreamWriter);
		writer.printDocument(document);
		writer.flush();
	}

	/** The cached session descriptor, or <code>null</code> */
	private RefactoringSessionDescriptor fCachedDescriptor= null;

	/** The cached document, or <code>null</code> */
	private Document fCachedDocument= null;

	/** The cached path, or <code>null</code> */
	private IPath fCachedPath= null;

	/** The cached file store, or <code>null</code> */
	private IFileStore fCachedStore= null;

	/** The history file store */
	private final IFileStore fHistoryStore;

	/**
	 * The non-empty name of the managed project, or <code>null</code> for the
	 * workspace
	 */
	private final String fProjectName;

	/**
	 * Creates a new refactoring history manager.
	 *
	 * @param store
	 *            the history file store
	 * @param name
	 *            the non-empty name of the managed project, or
	 *            <code>null</code> for the workspace
	 */
	RefactoringHistoryManager(final IFileStore store, final String name) {
		Assert.isNotNull(store);
		Assert.isTrue(name == null || !"".equals(name)); //$NON-NLS-1$
		fHistoryStore= store;
		fProjectName= name;
	}

	/**
	 * Adds the specified refactoring descriptor to the refactoring history.
	 *
	 * @param descriptor
	 *            the refactoring descriptor to add
	 * @param sort
	 *            <code>true</code> if the refactoring descriptor should be
	 *            inserted into the history according to its time stamp,
	 *            <code>false</code> if the descriptor is assumed to be the
	 *            most recent one, and its simply appended
	 * @param monitor
	 *            the progress monitor to use
	 * @throws CoreException
	 *             if an error occurs while adding the descriptor to the history
	 */
	void addRefactoringDescriptor(final RefactoringDescriptor descriptor, final boolean sort, final IProgressMonitor monitor) throws CoreException {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_updating_history, 18);
			final long stamp= descriptor.getTimeStamp();
			if (stamp >= 0) {
				final IPath path= stampToPath(stamp);
				final IFileStore folder= fHistoryStore.getFileStore(path);
				final IFileStore history= folder.getChild(RefactoringHistoryService.NAME_HISTORY_FILE);
				final IFileStore index= folder.getChild(RefactoringHistoryService.NAME_INDEX_FILE);
				final RefactoringDescriptorProxy[] proxies= new RefactoringDescriptorProxy[] { new DefaultRefactoringDescriptorProxy(descriptor.getDescription(), descriptor.getProject(), descriptor.getTimeStamp())};
				if (history.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists()) {
					InputStream input= null;
					try {
						input= new BufferedInputStream(history.openInputStream(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
						final Document document= getCachedDocument(path, input);
						try {
							input.close();
							input= null;
						} catch (IOException exception) {
							// Do nothing
						}
						monitor.worked(1);
						final Document result= transformDescriptor(descriptor, false);
						if (result != null) {
							boolean found= false;
							final NodeList list= result.getElementsByTagName(IRefactoringSerializationConstants.ELEMENT_REFACTORING);
							final Element root= document.getDocumentElement();
							if (sort) {
								final String string= Long.toString(stamp);
								int length= list.getLength();
								for (int offset= 0; offset < length; offset++) {
									final Element element= (Element) list.item(offset);
									final String attribute= element.getAttribute(IRefactoringSerializationConstants.ATTRIBUTE_STAMP);
									if (attribute != null) {
										if (string.compareTo(attribute) > 0) {
											root.insertBefore(document.importNode(element, true), element);
											found= true;
											break;
										}
									}
								}
							}
							if (!found)
								root.appendChild(document.importNode(list.item(0), true));
							writeHistoryEntry(history, document, new SubProgressMonitor(monitor, 10, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
							if (sort) {
								final Set<RefactoringDescriptorProxy> set= new HashSet<>(64);
								readRefactoringDescriptorProxies(index, null, set, 0, Long.MAX_VALUE, new SubProgressMonitor(monitor, 2), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
								writeIndexEntry(index, set.toArray(new RefactoringDescriptorProxy[set.size()]), EFS.NONE, new SubProgressMonitor(monitor, 3, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
							} else
								writeIndexEntry(index, proxies, EFS.APPEND, new SubProgressMonitor(monitor, 5, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
						}
					} catch (ParserConfigurationException | IOException | SAXException exception) {
						throw createCoreException(exception);
					} finally {
						if (input != null) {
							try {
								input.close();
							} catch (IOException exception) {
								// Do nothing
							}
						}
					}
				} else {
					try {
						final Document result= transformDescriptor(descriptor, false);
						writeHistoryEntry(history, result, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
							writeIndexEntry(index, proxies, EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
					} catch (IOException exception) {
						throw createCoreException(exception);
					}
				}
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 * Returns the cached refactoring history document.
	 *
	 * @param path
	 *            the path of the document
	 * @param input
	 *            the input stream where to read the document
	 * @return the cached refactoring history document
	 * @throws SAXException
	 *             if an error occurs while parsing the history entry
	 * @throws IOException
	 *             if an input/output error occurs
	 * @throws ParserConfigurationException
	 *             if an error occurs in the parser configuration
	 */
	private Document getCachedDocument(final IPath path, final InputStream input) throws SAXException, IOException, ParserConfigurationException {
		if (path.equals(fCachedPath) && fCachedDocument != null)
			return fCachedDocument;
		DocumentBuilder parser= DocumentBuilderFactory.newInstance().newDocumentBuilder();
		parser.setErrorHandler(new DefaultHandler());
		final Document document= parser.parse(new InputSource(input));
		fCachedDocument= document;
		fCachedPath= path;
		return document;
	}

	/**
	 * Returns the cached refactoring session descriptor.
	 *
	 * @param store
	 *            the file store of the descriptor
	 * @param projectName
	 *            project name, or <code>null</code> for the workspace
	 * @param input
	 *            the input stream where to read the descriptor
	 * @return the cached refactoring session descriptor
	 * @throws CoreException
	 *             if an error occurs while reading the session
	 */
	private RefactoringSessionDescriptor getCachedSession(final IFileStore store, String projectName, final InputStream input) throws CoreException {
		if (store.equals(fCachedStore) && fCachedDescriptor != null)
			return fCachedDescriptor;
		final RefactoringSessionDescriptor descriptor;
		try {
			descriptor= new RefactoringSessionReader(false, projectName).readSession(new InputSource(input));
			fCachedDescriptor= descriptor;
			fCachedStore= store;
			return descriptor;
		} catch (CoreException e) {
			throw new CoreException(new MultiStatus(
					RefactoringCorePlugin.getPluginId(),
					IRefactoringCoreStatusCodes.REFACTORING_HISTORY_IO_ERROR,
					new IStatus[] { e.getStatus() },
					Messages.format(RefactoringCoreMessages.RefactoringHistoryManager_error_reading_file, BasicElementLabels.getURLPart(store.toURI().toString())),
					null));
		}
	}

	/**
	 * Reads the refactoring history from disk.
	 *
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp, inclusive
	 * @param monitor
	 *            the progress monitor to use
	 * @return the refactoring history
	 */
	RefactoringHistory readRefactoringHistory(final long start, final long end, final IProgressMonitor monitor) {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_retrieving_history, 200);
			final Set<RefactoringDescriptorProxy> set= new HashSet<>();
			try {
				if (fHistoryStore.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 20, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists())
					readRefactoringDescriptorProxies(fHistoryStore, fProjectName, set, start, end, new SubProgressMonitor(monitor, 80), RefactoringCoreMessages.RefactoringHistoryService_retrieving_history);
				final IFileStore store= EFS.getLocalFileSystem().getStore(RefactoringCorePlugin.getDefault().getStateLocation()).getChild(RefactoringHistoryService.NAME_HISTORY_FOLDER).getChild(RefactoringHistoryService.NAME_WORKSPACE_PROJECT);
				if (store.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 20, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists())
					readRefactoringDescriptorProxies(store, null, set, start, end, new SubProgressMonitor(monitor, 80), RefactoringCoreMessages.RefactoringHistoryService_retrieving_history);
			} catch (CoreException exception) {
				RefactoringCorePlugin.log(exception);
			}
			final RefactoringDescriptorProxy[] proxies= new RefactoringDescriptorProxy[set.size()];
			set.toArray(proxies);
			return new RefactoringHistoryImplementation(proxies);
		} finally {
			monitor.done();
		}
	}

	/**
	 * Removes refactoring descriptors from the managed history.
	 * <p>
	 * All refactoring descriptors must be from the history entry denoted by the
	 * specified path.
	 * </p>
	 *
	 * @param proxies
	 *            the refactoring descriptors
	 * @param path
	 *            the path of the history entry
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label to use
	 * @throws CoreException
	 *             if an error occurs
	 */
	private void removeRefactoringDescriptors(final RefactoringDescriptorProxy[] proxies, final IPath path, final IProgressMonitor monitor, final String task) throws CoreException {
		try {
			monitor.beginTask(task, 5);
			final IFileStore folder= fHistoryStore.getFileStore(path);
			final IFileStore index= folder.getChild(RefactoringHistoryService.NAME_INDEX_FILE);
			if (index.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists()) {
				final Set<RefactoringDescriptorProxy> resultingProxies= new HashSet<>(64);
				readRefactoringDescriptorProxies(index, null, resultingProxies, 0, Long.MAX_VALUE, new SubProgressMonitor(monitor, 1), task);
				if (resultingProxies.size() == proxies.length)
					removeIndexTree(folder, new SubProgressMonitor(monitor, 1), task);
				else {
					final IFileStore history= folder.getChild(RefactoringHistoryService.NAME_HISTORY_FILE);
					if (history.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists()) {
						InputStream input= null;
						Document document= null;
						try {
							input= new BufferedInputStream(history.openInputStream(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
							document= getCachedDocument(path, input);
						} catch (ParserConfigurationException | IOException | SAXException exception) {
							throw createCoreException(exception);
						} finally {
							if (input != null) {
								try {
									input.close();
								} catch (IOException exception) {
									// Do nothing
								}
							}
						}
						final Set<Node> removedNodes= new HashSet<>(proxies.length);
						final NodeList list= document.getElementsByTagName(IRefactoringSerializationConstants.ELEMENT_REFACTORING);
						final int length= list.getLength();
						for (int offset= 0; offset < length; offset++) {
							final Node node= list.item(offset);
							final NamedNodeMap attributes= node.getAttributes();
							if (attributes != null) {
								final Node item= attributes.getNamedItem(IRefactoringSerializationConstants.ATTRIBUTE_STAMP);
								if (item != null) {
									final String value= item.getNodeValue();
									if (value != null) {
										for (RefactoringDescriptorProxy proxy : proxies) {
											final long stamp= proxy.getTimeStamp();
											if (value.equals(String.valueOf(stamp))) {
												resultingProxies.remove(new DefaultRefactoringDescriptorProxy(proxy.getDescription(), proxy.getProject(), stamp));
												removedNodes.add(node);
											}
										}
									}
								}
							}
						}
						for (Node node : removedNodes) {
							node.getParentNode().removeChild(node);
						}
						try {
							writeIndexEntry(index, resultingProxies.toArray(new RefactoringDescriptorProxy[resultingProxies.size()]), EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), task);
							writeHistoryEntry(history, document, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), task);
						} catch (IOException exception) {
							throw createCoreException(exception);
						}
					}
				}
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 * Removes refactoring descriptors from the managed history.
	 *
	 * @param proxies
	 *            the refactoring descriptors
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label to use
	 * @throws CoreException
	 *             if an error occurs
	 */
	void removeRefactoringDescriptors(final RefactoringDescriptorProxy[] proxies, final IProgressMonitor monitor, final String task) throws CoreException {
		try {
			final Map<IPath, Collection<RefactoringDescriptorProxy>> paths= new HashMap<>();
			monitor.beginTask(task, proxies.length + 300);
			for (RefactoringDescriptorProxy proxy : proxies) {
				final IPath path= stampToPath(proxy.getTimeStamp());
				Collection<RefactoringDescriptorProxy> collection= paths.get(path);
				if (collection == null) {
					collection= new ArrayList<>(64);
					paths.put(path, collection);
				}
				collection.add(proxy);
			}
			final IProgressMonitor subMonitor= new SubProgressMonitor(monitor, 300);
			try {
				final Set<Entry<IPath, Collection<RefactoringDescriptorProxy>>> entries= paths.entrySet();
				subMonitor.beginTask(task, entries.size());
				for (Entry<IPath, Collection<RefactoringDescriptorProxy>> entry : entries) {
					final Collection<RefactoringDescriptorProxy> collection= entry.getValue();
					removeRefactoringDescriptors(collection.toArray(new RefactoringDescriptorProxy[collection.size()]), entry.getKey(), new SubProgressMonitor(subMonitor, 1), task);
				}
			} finally {
				subMonitor.done();
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 * Requests the resolved refactoring descriptor associated with the given
	 * proxy.
	 *
	 * @param proxy
	 *            the refactoring descriptor proxy
	 * @param monitor
	 *            the progress monitor to use
	 * @return the associated refactoring descriptor, or <code>null</code>
	 */
	RefactoringDescriptor requestDescriptor(final RefactoringDescriptorProxy proxy, final IProgressMonitor monitor) {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_resolving_information, 2);
			final long stamp= proxy.getTimeStamp();
			if (stamp >= 0) {
				InputStream input= null;
				try {
					final IFileStore folder= fHistoryStore.getFileStore(stampToPath(stamp));
					final IFileStore file= folder.getChild(RefactoringHistoryService.NAME_HISTORY_FILE);
					if (file.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists()) {
						input= new BufferedInputStream(file.openInputStream(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
						final RefactoringSessionDescriptor descriptor= getCachedSession(file, fProjectName, input);
						if (descriptor != null) {
							final RefactoringDescriptor[] descriptors= descriptor.getRefactorings();
							for (RefactoringDescriptor refactoringDescriptor : descriptors) {
								if (refactoringDescriptor.getTimeStamp() == stamp) {
									return refactoringDescriptor;
								}
							}
						}
					}
				} catch (CoreException exception) {
					RefactoringCorePlugin.log(exception);
				} finally {
					try {
						if (input != null)
							input.close();
					} catch (IOException exception) {
						RefactoringCorePlugin.log(exception);
					}
				}
			}
		} finally {
			monitor.done();
		}
		return null;
	}

	/**
	 * Sets the comment of the specified refactoring.
	 *
	 * @param proxy
	 *            the refactoring descriptor proxy
	 * @param comment
	 *            the comment
	 * @param monitor
	 *            the progress monitor to use
	 * @throws CoreException
	 *             if an error occurs while setting the comment
	 */
	void setComment(final RefactoringDescriptorProxy proxy, final String comment, final IProgressMonitor monitor) throws CoreException {
		try {
			monitor.beginTask(RefactoringCoreMessages.RefactoringHistoryService_updating_history, 100);
			final long stamp= proxy.getTimeStamp();
			if (stamp >= 0) {
				final IPath path= stampToPath(stamp);
				final IFileStore folder= fHistoryStore.getFileStore(path);
				final IFileStore history= folder.getChild(RefactoringHistoryService.NAME_HISTORY_FILE);
				if (history.fetchInfo(EFS.NONE, new SubProgressMonitor(monitor, 20, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)).exists()) {
					InputStream input= null;
					try {
						input= new BufferedInputStream(history.openInputStream(EFS.NONE, new SubProgressMonitor(monitor, 40, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
						final Document document= getCachedDocument(path, input);
						try {
							input.close();
							input= null;
						} catch (IOException exception) {
							// Do nothing
						}
						final String time= String.valueOf(stamp);
						final NodeList list= document.getElementsByTagName(IRefactoringSerializationConstants.ELEMENT_REFACTORING);
						int length= list.getLength();
						for (int index= 0; index < length; index++) {
							final Element element= (Element) list.item(index);
							if (time.equals(element.getAttribute(IRefactoringSerializationConstants.ATTRIBUTE_STAMP))) {
								element.setAttribute(IRefactoringSerializationConstants.ATTRIBUTE_COMMENT, comment);
								break;
							}
						}
						writeHistoryEntry(history, document, new SubProgressMonitor(monitor, 40, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), RefactoringCoreMessages.RefactoringHistoryService_updating_history);
					} catch (ParserConfigurationException | IOException | SAXException exception) {
						throw createCoreException(exception);
					} finally {
						if (input != null) {
							try {
								input.close();
							} catch (IOException exception) {
								// Do nothing
							}
						}
					}
				}
			}
		} finally {
			monitor.done();
		}
	}

	/**
	 * Writes the specified document node into the refactoring history.
	 *
	 * @param file
	 *            the refactoring history file
	 * @param document
	 *            the document representing the history entry
	 * @param monitor
	 *            the progress monitor to use
	 * @param task
	 *            the task label
	 * @throws CoreException
	 *             if an error occurs while adding the history entry
	 */
	private void writeHistoryEntry(final IFileStore file, final Document document, final IProgressMonitor monitor, final String task) throws CoreException {
		OutputStream output= null;
		try {
			monitor.beginTask(task, 2);
				file.getParent().mkdir(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
				output= new BufferedOutputStream(file.openOutputStream(EFS.NONE, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
			writeNode(output, document);
				} finally {
					fCachedDocument= null;
					fCachedPath= null;
					fCachedDescriptor= null;
					fCachedStore= null;
				if (output != null) {
					try {
						output.close();
					} catch (IOException exception) {
						// Do nothing
					}
				}
			monitor.done();
		}
	}
}
