/*******************************************************************************
 * 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
 *     Ed Swartz <ed.swartz@nokia.com> -
 *         (bug 157203: [ltk] [patch] TextEditBasedChange/TextChange provides incorrect diff when one side is empty)
 *******************************************************************************/
package org.eclipse.ltk.core.refactoring;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditCopier;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.text.edits.TextEditProcessor;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;

import org.eclipse.ltk.internal.core.refactoring.Changes;

/**
 * An abstract base implementation of a change which is based on text edits.
 *
 * @since 3.2
 */
public abstract class TextEditBasedChange extends Change {

	/**
	 * Text edit processor which has the ability to selectively include or exclude single text edits.
	 */
	static final class LocalTextEditProcessor extends TextEditProcessor {
		public static final int EXCLUDE= 1;
		public static final int INCLUDE= 2;

		private TextEdit[] fExcludes;
		private TextEdit[] fIncludes;

		protected LocalTextEditProcessor(IDocument document, TextEdit root, int flags) {
			super(document, root, flags);
		}
		public void setIncludes(TextEdit[] includes) {
			Assert.isNotNull(includes);
			Assert.isTrue(fExcludes == null);
			fIncludes= flatten(includes);
		}
		public void setExcludes(TextEdit[] excludes) {
			Assert.isNotNull(excludes);
			Assert.isTrue(fIncludes == null);
			fExcludes= flatten(excludes);
		}
		@Override
		protected boolean considerEdit(TextEdit edit) {
			if (fExcludes != null) {
				for (TextEdit fExclude : fExcludes) {
					if (edit.equals(fExclude)) {
						return false;
					}
				}
				return true;
			}
			if (fIncludes != null) {
				for (TextEdit fInclude : fIncludes) {
					if (edit.equals(fInclude)) {
						return true;
					}
				}
				return false;
			}
			return true;
		}
		private TextEdit[] flatten(TextEdit[] edits) {
			List<TextEdit> result= new ArrayList<>(5);
			for (TextEdit edit : edits) {
				flatten(result, edit);
			}
			return result.toArray(new TextEdit[result.size()]);
		}
		private void flatten(List<TextEdit> result, TextEdit edit) {
			result.add(edit);
			TextEdit[] children= edit.getChildren();
			for (TextEdit child : children) {
				flatten(result, child);
			}
		}
	}

	/**
	 * Value objects encapsulating a document with an associated region.
	 */
	static final class PreviewAndRegion {
		public IDocument document;
		public IRegion region;
		public PreviewAndRegion(IDocument d, IRegion r) {
			document= d;
			region= r;
		}
	}

	/**
	 * A special object denoting all edits managed by the change. This even
	 * includes those edits not managed by a {@link TextEditBasedChangeGroup}.
	 */
	static final TextEditBasedChangeGroup[] ALL_EDITS= new TextEditBasedChangeGroup[0];

	/** The list of change groups */
	private List<TextEditBasedChangeGroup> fChangeGroups;
	private GroupCategorySet fCombiedGroupCategories;

	/** The name of the change */
	private String fName;

	/** The text type */
	private String fTextType;

	/** Should the positions of edits be tracked during change generation? */
	private boolean fTrackEdits;

	/**
	 * Creates a new abstract text edit change with the specified name.  The name is a
	 * human-readable value that is displayed to users.  The name does not
	 * need to be unique, but it must not be <code>null</code>.
	 * <p>
	 * The text type of this text edit change is set to <code>txt</code>.
	 * </p>
	 *
	 * @param name the name of the text edit change
	 *
	 * @see #setTextType(String)
	 */
	protected TextEditBasedChange(String name) {
		Assert.isNotNull(name, "Name must not be null"); //$NON-NLS-1$
		fChangeGroups= new ArrayList<>(5);
		fName= name;
		fTextType= "txt"; //$NON-NLS-1$
	}

	/**
	 * Adds a {@link TextEditBasedChangeGroup text edit change group}.
	 * The edits managed by the given text edit change group must be part of
	 * the change's root edit.
	 *
	 * @param group the text edit change group to add
	 */
	public void addChangeGroup(TextEditBasedChangeGroup group) {
		Assert.isTrue(group != null);
		fChangeGroups.add(group);
		if (fCombiedGroupCategories != null) {
			fCombiedGroupCategories= GroupCategorySet.union(fCombiedGroupCategories, group.getGroupCategorySet());
		}
	}

	/**
	 * Adds a {@link TextEditGroup text edit group}. This method is a convenience
	 * method for calling <code>change.addChangeGroup(new
	 * TextEditBasedChangeGroup(change, group));</code>.
	 *
	 * @param group the text edit group to add
	 */
	public void addTextEditGroup(TextEditGroup group) {
		addChangeGroup(new TextEditBasedChangeGroup(this, group));
	}

	/**
	 * Returns <code>true</code> if the change has one of the given group
	 * categories. Otherwise <code>false</code> is returned.
	 *
	 * @param groupCategories the group categories to check
	 *
	 * @return whether the change has one of the given group
	 *  categories
	 *
	 * @since 3.2
	 */
	public boolean hasOneGroupCategory(List<GroupCategory> groupCategories) {
		if (fCombiedGroupCategories == null) {
			fCombiedGroupCategories= GroupCategorySet.NONE;
			for (TextEditBasedChangeGroup group : fChangeGroups) {
				fCombiedGroupCategories= GroupCategorySet.union(fCombiedGroupCategories, group.getGroupCategorySet());
			}
		}
		return fCombiedGroupCategories.containsOneCategory(groupCategories);
	}

	/**
	 * Returns the {@link TextEditBasedChangeGroup text edit change groups} managed by this
	 * buffer change.
	 *
	 * @return the text edit change groups
	 */
	public final TextEditBasedChangeGroup[] getChangeGroups() {
		return fChangeGroups.toArray(new TextEditBasedChangeGroup[fChangeGroups.size()]);
	}

	String getContent(IDocument document, IRegion region, boolean expandRegionToFullLine, int surroundingLines) throws CoreException {
		try {
			if (expandRegionToFullLine) {
				int startLine= Math.max(document.getLineOfOffset(region.getOffset()) - surroundingLines, 0);
				int endLine;
				if (region.getLength() == 0) {
					// no lines are in the region, so remove one from the context,
					// or else spurious changes show up that look like deletes from the source
					if (surroundingLines == 0) {
						// empty: show nothing
						return ""; //$NON-NLS-1$
					}

					endLine= Math.min(
						document.getLineOfOffset(region.getOffset()) + surroundingLines - 1,
						document.getNumberOfLines() - 1);
				} else {
					endLine= Math.min(
						document.getLineOfOffset(region.getOffset() + region.getLength() - 1) + surroundingLines,
						document.getNumberOfLines() - 1);
				}

				int offset= document.getLineInformation(startLine).getOffset();
				IRegion endLineRegion= document.getLineInformation(endLine);
				int length= endLineRegion.getOffset() + endLineRegion.getLength() - offset;
				return document.get(offset, length);

			} else {
				return document.get(region.getOffset(), region.getLength());
			}
		} catch (BadLocationException e) {
			throw Changes.asCoreException(e);
		}
	}

	/**
	 * Returns the current content of the document this text
	 * change is associated with.
	 *
	 * @param pm a progress monitor to report progress or <code>null</code>
	 *  if no progress reporting is desired
	 * @return the current content of the text edit change
	 *
	 * @exception CoreException if the content can't be accessed
	 */
	public abstract String getCurrentContent(IProgressMonitor pm) throws CoreException;

	/**
	 * Returns the current content of the text edit change clipped to a specific
	 * region. The region is determined as follows:
	 * <ul>
	 *   <li>if <code>expandRegionToFullLine</code> is <code>false</code>
	 *       then the parameter <code>region</code> determines the clipping.
	 *   </li>
	 *   <li>if <code>expandRegionToFullLine</code> is <code>true</code>
	 *       then the region determined by the parameter <code>region</code>
	 *       is extended to cover full lines.
	 *   </li>
	 *   <li>if <code>surroundingLines</code> &gt; 0 then the given number
	 *       of surrounding lines is added. The value of <code>surroundingLines
	 *       </code> is only considered if <code>expandRegionToFullLine</code>
	 *       is <code>true</code>
	 *   </li>
	 * </ul>
	 *
	 * @param region the starting region for the text to be returned
	 * @param expandRegionToFullLine if <code>true</code> is passed the region
	 *  is extended to cover full lines
	 * @param surroundingLines the number of surrounding lines to be added to
	 *  the clipping region. Is only considered if <code>expandRegionToFullLine
	 *  </code> is <code>true</code>
	 * @param pm a progress monitor to report progress or <code>null</code>
	 *  if no progress reporting is desired
	 *
	 * @return the current content of the text edit change clipped to a region
	 *  determined by the given parameters.
	 *
	 * @throws CoreException if an exception occurs while accessing the current content
	 */
	public abstract String getCurrentContent(IRegion region, boolean expandRegionToFullLine, int surroundingLines, IProgressMonitor pm) throws CoreException;

	/**
	 * Returns whether preview edits are remembered for further region
	 * tracking or not.
	 *
	 * @return <code>true</code> if executed text edits are remembered
	 * during preview generation; otherwise <code>false</code>
	 */
	public boolean getKeepPreviewEdits() {
		return fTrackEdits;
	}

	@Override
	public String getName() {
		return fName;
	}

	/**
	 * Returns a preview of the text edit change clipped to a specific region.
	 * The preview is created by applying the text edits managed by the
	 * given array of {@link TextEditBasedChangeGroup text edit change groups}.
	 * The region is determined as follows:
	 * <ul>
	 *   <li>if <code>expandRegionToFullLine</code> is <code>false</code>
	 *       then the parameter <code>region</code> determines the clipping.
	 *   </li>
	 *   <li>if <code>expandRegionToFullLine</code> is <code>true</code>
	 *       then the region determined by the parameter <code>region</code>
	 *       is extended to cover full lines.
	 *   </li>
	 *   <li>if <code>surroundingLines</code> &gt; 0 then the given number
	 *       of surrounding lines is added. The value of <code>surroundingLines
	 *       </code> is only considered if <code>expandRegionToFullLine</code>
	 *       is <code>true</code>
	 *   </li>
	 * </ul>
	 *
	 * @param changeGroups a set of change groups for which a preview is to be
	 *  generated
	 * @param region the starting region for the clipping
	 * @param expandRegionToFullLine if <code>true</code> is passed the region
	 *  is extended to cover full lines
	 * @param surroundingLines the number of surrounding lines to be added to
	 *  the clipping region. Is only considered if <code>expandRegionToFullLine
	 *  </code> is <code>true</code>
	 * @param pm a progress monitor to report progress or <code>null</code>
	 *  if no progress reporting is desired
	 *
	 * @return the current content of the text change clipped to a region
	 *  determined by the given parameters.
	 *
	 * @throws CoreException if an exception occurs while generating the preview
	 *
	 * @see #getCurrentContent(IRegion, boolean, int, IProgressMonitor)
	 */
	public abstract String getPreviewContent(TextEditBasedChangeGroup[] changeGroups, IRegion region, boolean expandRegionToFullLine, int surroundingLines, IProgressMonitor pm) throws CoreException;

	/**
	 * Returns the preview content as a string.
	 *
	 * @param pm a progress monitor to report progress or <code>null</code>
	 *  if no progress reporting is desired
	 * @return the preview
	 *
	 * @throws CoreException if the preview can't be created
	 */
	public abstract String getPreviewContent(IProgressMonitor pm) throws CoreException;

	/**
	 * Returns the text edit change's text type.
	 *
	 * @return the text edit change's text type
	 */
	public String getTextType() {
		return fTextType;
	}

	TextEdit[] mapEdits(TextEdit[] edits, TextEditCopier copier) {
		if (edits == null)
			return null;
		final List<TextEdit> result= new ArrayList<>(edits.length);
		for (TextEdit e : edits) {
			TextEdit edit= copier.getCopy(e);
			if (edit != null)
				result.add(edit);
		}
		return result.toArray(new TextEdit[result.size()]);
	}

	@Override
	public void setEnabled(boolean enabled) {
		super.setEnabled(enabled);
		for (TextEditBasedChangeGroup element : fChangeGroups) {
			element.setEnabled(enabled);
		}
	}

	/**
	 * Controls whether the text edit change should keep executed edits during
	 * preview generation.
	 *
	 * @param keep if <code>true</code> executed preview edits are kept
	 */
	public void setKeepPreviewEdits(boolean keep) {
		fTrackEdits= keep;
	}

	/**
	 * Sets the text type. The text type is used to determine the content
	 * merge viewer used to present the difference between the original
	 * and the preview content in the user interface. Content merge viewers
	 * are defined via the extension point <code>org.eclipse.compare.contentMergeViewers</code>.
	 * <p>
	 * The default text type is <code>txt</code>.
	 * </p>
	 *
	 * @param type the text type. If <code>null</code> is passed the text type is
	 *  reseted to the default text type <code>txt</code>.
	 */
	public void setTextType(String type) {
		if (type == null)
			type= "txt"; //$NON-NLS-1$
		fTextType= type;
	}
}
