/*******************************************************************************
 * Copyright (c) 2000, 2020 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.jdt.internal.corext.fix;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.swt.widgets.Display;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.text.edits.UndoEdit;

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

import org.eclipse.ltk.core.refactoring.CategorizedTextEditGroup;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.ContentStamp;
import org.eclipse.ltk.core.refactoring.GroupCategory;
import org.eclipse.ltk.core.refactoring.GroupCategorySet;
import org.eclipse.ltk.core.refactoring.NullChange;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringTickProvider;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.TextEditBasedChangeGroup;
import org.eclipse.ltk.core.refactoring.TextFileChange;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.dom.ASTBatchParser;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.changes.DynamicValidationStateChange;
import org.eclipse.jdt.internal.corext.refactoring.changes.MultiStateCompilationUnitChange;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.corext.refactoring.util.TextEditUtil;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.cleanup.CleanUpContext;
import org.eclipse.jdt.ui.cleanup.CleanUpOptions;
import org.eclipse.jdt.ui.cleanup.ICleanUp;
import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
import org.eclipse.jdt.ui.text.java.IProblemLocation;

import org.eclipse.jdt.internal.ui.IJavaStatusConstants;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.fix.IMultiFix.MultiFixContext;
import org.eclipse.jdt.internal.ui.fix.MapCleanUpOptions;
import org.eclipse.jdt.internal.ui.refactoring.IScheduledRefactoring;

public class CleanUpRefactoring extends Refactoring implements IScheduledRefactoring {

	public static class CleanUpTarget {

		private final ICompilationUnit fCompilationUnit;

		public CleanUpTarget(ICompilationUnit unit) {
			fCompilationUnit= unit;
		}

		public ICompilationUnit getCompilationUnit() {
			return fCompilationUnit;
		}
	}

	public static class MultiFixTarget extends CleanUpTarget {

		private final IProblemLocation[] fProblems;

		public MultiFixTarget(ICompilationUnit unit, IProblemLocation[] problems) {
			super(unit);
			fProblems= problems;
		}

		public IProblemLocation[] getProblems() {
			return fProblems;
		}
	}

	public static class CleanUpChange extends CompilationUnitChange {

		private UndoEdit fUndoEdit;

		public CleanUpChange(String name, ICompilationUnit cunit) {
	        super(name, cunit);
        }

		@Override
		protected Change createUndoChange(UndoEdit edit, ContentStamp stampToRestore) {
		    fUndoEdit= edit;
			return super.createUndoChange(edit, stampToRestore);
		}

		public UndoEdit getUndoEdit() {
        	return fUndoEdit;
        }

		/*
		 * @see org.eclipse.ltk.core.refactoring.TextChange#perform(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@Override
		public Change perform(final IProgressMonitor pm) throws CoreException {
			if (Display.getCurrent() == null) {
				final Change[] result= new Change[1];
				final CoreException[] exs= new CoreException[1];
				Display.getDefault().syncExec(() -> {
					try {
						result[0]= CleanUpChange.super.perform(pm);
					} catch (CoreException e) {
						exs[0]= e;
					}
				});

				if (exs[0] != null) {
					IStatus status= new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, Messages.format(FixMessages.CleanUpRefactoring_exception,
							getCompilationUnit().getResource().getFullPath().toString()), exs[0]);
					throw new CoreException(status);
				}

				return result[0];
			} else {
				return super.perform(pm);
			}
		}
	}

	private static class FixCalculationException extends RuntimeException {

		private static final long serialVersionUID= 3807273310144726165L;

		private final CoreException fException;

		public FixCalculationException(CoreException exception) {
			fException= exception;
		}

		public CoreException getException() {
			return fException;
		}
	}

	private static class ParseListElement {

		private final CleanUpTarget fTarget;
		private final ICleanUp[] fCleanUpsArray;

		public ParseListElement(CleanUpTarget cleanUpTarget, ICleanUp[] cleanUps) {
			fTarget= cleanUpTarget;
			fCleanUpsArray= cleanUps;
		}

		public CleanUpTarget getTarget() {
			return fTarget;
		}

		public ICleanUp[] getCleanUps() {
			return fCleanUpsArray;
		}
	}

	private final static class CleanUpRefactoringProgressMonitor extends SubProgressMonitor {

		private double fRealWork;
		private int fFlushCount;
		private final int fSize;
		private final int fIndex;

		private CleanUpRefactoringProgressMonitor(IProgressMonitor monitor, int ticks, int size, int index) {
			super(monitor, ticks);
			fFlushCount= 0;
			fSize= size;
			fIndex= index;
		}

		@Override
		public void internalWorked(double work) {
			fRealWork+= work;
		}

		public void flush() {
			super.internalWorked(fRealWork);
			reset();
			fFlushCount++;
		}

		public void reset() {
			fRealWork= 0.0;
		}

		@Override
		public void done() {}

		public int getIndex() {
			return fIndex + fFlushCount;
		}

		public String getSubTaskMessage(ICompilationUnit source) {
			String typeName= BasicElementLabels.getFileName(source);
			return Messages.format(FixMessages.CleanUpRefactoring_ProcessingCompilationUnit_message, new Object[] {Integer.valueOf(getIndex()), Integer.valueOf(fSize), typeName});
		}
	}

	private static class CleanUpASTRequestor extends ASTRequestor {

		private final List<ParseListElement> fUndoneElements;
		private final Hashtable<ICompilationUnit, List<CleanUpChange>> fSolutions;
		private final Hashtable<ICompilationUnit, ParseListElement> fCompilationUnitParseElementMap;
		private final CleanUpRefactoringProgressMonitor fMonitor;

		public CleanUpASTRequestor(List<ParseListElement> parseList, Hashtable<ICompilationUnit, List<CleanUpChange>> solutions, CleanUpRefactoringProgressMonitor monitor) {
			fSolutions= solutions;
			fMonitor= monitor;
			fUndoneElements= new ArrayList<>();
			fCompilationUnitParseElementMap= new Hashtable<>(parseList.size());
			for (ParseListElement element : parseList) {
				fCompilationUnitParseElementMap.put(element.getTarget().getCompilationUnit(), element);
			}
		}

		@Override
		public void acceptAST(ICompilationUnit source, CompilationUnit ast) {

			fMonitor.subTask(fMonitor.getSubTaskMessage(source));

			ICompilationUnit primary= (ICompilationUnit)source.getPrimaryElement();
			ParseListElement element= fCompilationUnitParseElementMap.get(primary);
			CleanUpTarget target= element.getTarget();

			CleanUpContext context;
			if (target instanceof MultiFixTarget) {
				context= new MultiFixContext(source, ast, ((MultiFixTarget)target).getProblems());
			} else {
				context= new CleanUpContext(source, ast);
			}
			ICleanUp[] rejectedCleanUps= calculateSolutions(context, element.getCleanUps());

			if (rejectedCleanUps.length > 0) {
				fUndoneElements.add(new ParseListElement(target, rejectedCleanUps));
				fMonitor.reset();
			} else {
				fMonitor.flush();
			}
		}

		public void acceptSource(ICompilationUnit source) {
			acceptAST(source, null);
		}

		public List<ParseListElement> getUndoneElements() {
			return fUndoneElements;
		}

		private ICleanUp[] calculateSolutions(CleanUpContext context, ICleanUp[] cleanUps) {
			List<ICleanUp>result= new ArrayList<>();
			CleanUpChange solution;
			try {
				solution= calculateChange(context, cleanUps, result, null);
			} catch (CoreException e) {
				throw new FixCalculationException(e);
			}

			if (solution != null) {
				integrateSolution(solution, context.getCompilationUnit());
			}

			return result.toArray(new ICleanUp[result.size()]);
		}

		private void integrateSolution(CleanUpChange solution, ICompilationUnit source) {
			ICompilationUnit primary= source.getPrimary();

			List<CleanUpChange> changes= fSolutions.get(primary);
			if (changes == null) {
				changes= new ArrayList<>();
				fSolutions.put(primary, changes);
			}
			changes.add(solution);
		}
	}

	private class CleanUpFixpointIterator {

		private List<ParseListElement> fParseList;
		private final Hashtable<ICompilationUnit, List<CleanUpChange>> fSolutions;
		private final Hashtable<ICompilationUnit, ICompilationUnit> fWorkingCopies; // map from primary to working copy
		private final Map<String, String> fCleanUpOptions;
		private final int fSize;
		private int fIndex;

		public CleanUpFixpointIterator(CleanUpTarget[] targets, ICleanUp[] cleanUps) {
			fSolutions= new Hashtable<>(targets.length);
			fWorkingCopies= new Hashtable<>();

			fParseList= new ArrayList<>(targets.length);
			for (CleanUpTarget target : targets) {
				fParseList.add(new ParseListElement(target, cleanUps));
			}

			fCleanUpOptions= new Hashtable<>();
			for (ICleanUp cleanUp : cleanUps) {
				Map<String, String> currentCleanUpOption= cleanUp.getRequirements().getCompilerOptions();
				if (currentCleanUpOption != null)
					fCleanUpOptions.putAll(currentCleanUpOption);
			}

			fSize= targets.length;
			fIndex= 1;
		}

		public boolean hasNext() {
			return !fParseList.isEmpty();
		}

		public void next(IProgressMonitor monitor) throws CoreException {
			List<ICompilationUnit> parseList= new ArrayList<>();
			List<ICompilationUnit> sourceList= new ArrayList<>();

			try {
				for (ParseListElement element : fParseList) {
					ICompilationUnit compilationUnit= element.getTarget().getCompilationUnit();
					if (fSolutions.containsKey(compilationUnit)) {
						if (fWorkingCopies.containsKey(compilationUnit)) {
							compilationUnit= fWorkingCopies.get(compilationUnit);
						} else {
							compilationUnit= compilationUnit.getWorkingCopy(new WorkingCopyOwner() {}, null);
							fWorkingCopies.put(compilationUnit.getPrimary(), compilationUnit);
						}
						applyChange(compilationUnit, fSolutions.get(compilationUnit.getPrimary()));
					}

					if (requiresAST(element.getCleanUps())) {
						parseList.add(compilationUnit);
					} else {
						sourceList.add(compilationUnit);
					}
				}

				CleanUpRefactoringProgressMonitor cuMonitor= new CleanUpRefactoringProgressMonitor(monitor, parseList.size() + sourceList.size(), fSize, fIndex);
				CleanUpASTRequestor requestor= new CleanUpASTRequestor(fParseList, fSolutions, cuMonitor);
				if (parseList.size() > 0) {
					ASTBatchParser parser= new ASTBatchParser() {
						@Override
						protected ASTParser createParser(IJavaProject project) {
							ASTParser result= createCleanUpASTParser();
							result.setProject(project);

							Map<String, String> options= RefactoringASTParser.getCompilerOptions(project);
							options.putAll(fCleanUpOptions);
							result.setCompilerOptions(options);
							return result;
						}
					};
					try {
						ICompilationUnit[] units= parseList.toArray(new ICompilationUnit[parseList.size()]);
						parser.createASTs(units, new String[0], requestor, cuMonitor);
					} catch (FixCalculationException e) {
						throw e.getException();
					}
				}

				for (ICompilationUnit cu : sourceList) {
					monitor.worked(1);

					requestor.acceptSource(cu);

					if (monitor.isCanceled())
						throw new OperationCanceledException();
				}

				fParseList= requestor.getUndoneElements();
				fIndex= cuMonitor.getIndex();
			} finally {
			}
		}

		public void dispose() {
			for (ICompilationUnit cu : fWorkingCopies.values()) {
				try {
					cu.discardWorkingCopy();
				} catch (JavaModelException e) {
					JavaPlugin.log(e);
				}
			}
			fWorkingCopies.clear();
		}

		private boolean requiresAST(ICleanUp[] cleanUps) {
			for (ICleanUp cleanUp : cleanUps) {
				if (cleanUp.getRequirements().requiresAST()) {
					return true;
				}
			}
			return false;
		}

		public Change[] getResult() {

			Change[] result= new Change[fSolutions.size()];
			int i=0;
			for (Entry<ICompilationUnit, List<CleanUpChange>> entry : fSolutions.entrySet()) {
				List<CleanUpChange> changes= entry.getValue();
				ICompilationUnit unit= entry.getKey();

				int saveMode;
				if (fLeaveFilesDirty) {
					saveMode= TextFileChange.LEAVE_DIRTY;
				} else {
					saveMode= TextFileChange.KEEP_SAVE_STATE;
				}

				if (changes.size() == 1) {
					CleanUpChange change= changes.get(0);
					change.setSaveMode(saveMode);
					result[i]= change;
				} else {
					MultiStateCompilationUnitChange mscuc= new MultiStateCompilationUnitChange(getChangeName(unit), unit);
					for (CleanUpChange change : changes) {
						mscuc.addChange(createGroupFreeChange(change));
					}
					mscuc.setSaveMode(saveMode);
					result[i]= mscuc;
				}

				i++;
			}

			return result;
		}

		private TextChange createGroupFreeChange(CleanUpChange change) {
			CleanUpChange result= new CleanUpChange(change.getName(), change.getCompilationUnit());
			result.setEdit(change.getEdit());
			result.setSaveMode(change.getSaveMode());
	        return result;
        }

		private void applyChange(ICompilationUnit compilationUnit, List<CleanUpChange> changes) throws JavaModelException, CoreException {
			IDocument document= new Document(changes.get(0).getCurrentContent(new NullProgressMonitor()));
			for (CleanUpChange change : changes) {
				TextEdit edit= change.getEdit().copy();

				try {
					edit.apply(document, TextEdit.UPDATE_REGIONS);
				} catch (MalformedTreeException | BadLocationException e) {
					JavaPlugin.log(e);
				}
			}
			compilationUnit.getBuffer().setContents(document.get());
		}
	}

	private static final RefactoringTickProvider CLEAN_UP_REFACTORING_TICK_PROVIDER= new RefactoringTickProvider(0, 1, 0, 0);

	/**
	 * A clean up is considered slow if its execution lasts longer then the value of
	 * SLOW_CLEAN_UP_THRESHOLD in ms.
	 */
	private static final int SLOW_CLEAN_UP_THRESHOLD= 2000;

	private final List<ICleanUp> fCleanUps;
	private final Hashtable<IJavaProject, List<CleanUpTarget>> fProjects;
	private Change fChange;
	private boolean fLeaveFilesDirty;
	private final String fName;

	private boolean fUseOptionsFromProfile;

	public CleanUpRefactoring() {
		this(FixMessages.CleanUpRefactoring_Refactoring_name);
	}

	public CleanUpRefactoring(String name) {
		fName= name;
		fCleanUps= new ArrayList<>();
		fProjects= new Hashtable<>();
		fUseOptionsFromProfile= false;
	}

	public void setUseOptionsFromProfile(boolean enabled) {
		fUseOptionsFromProfile= enabled;
	}

	public void addCompilationUnit(ICompilationUnit unit) {
		addCleanUpTarget(new CleanUpTarget(unit));
	}

	public void addCleanUpTarget(CleanUpTarget target) {

		IJavaProject javaProject= target.getCompilationUnit().getJavaProject();
		if (!fProjects.containsKey(javaProject))
			fProjects.put(javaProject, new ArrayList<CleanUpTarget>());

		List<CleanUpTarget> targets= fProjects.get(javaProject);
		targets.add(target);
	}

	public CleanUpTarget[] getCleanUpTargets() {
		List<CleanUpTarget> result= new ArrayList<>();
		for (List<CleanUpTarget> projectTargets : fProjects.values()) {
			result.addAll(projectTargets);
		}
		return result.toArray(new CleanUpTarget[result.size()]);
	}

	public int getCleanUpTargetsSize() {
		int result= 0;
		for (List<CleanUpTarget> projectTargets : fProjects.values()) {
			result+= projectTargets.size();
		}
		return result;
	}

	public void addCleanUp(ICleanUp fix) {
		fCleanUps.add(fix);
	}

	public void clearCleanUps() {
		fCleanUps.clear();
	}

	public boolean hasCleanUps() {
		return !fCleanUps.isEmpty();
	}

	public ICleanUp[] getCleanUps() {
		return fCleanUps.toArray(new ICleanUp[fCleanUps.size()]);
	}

	public IJavaProject[] getProjects() {
		return fProjects.keySet().toArray(new IJavaProject[fProjects.size()]);
	}

	public void setLeaveFilesDirty(boolean leaveFilesDirty) {
		fLeaveFilesDirty= leaveFilesDirty;
	}

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

	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		if (pm != null) {
			pm.beginTask("", 1); //$NON-NLS-1$
			pm.worked(1);
			pm.done();
		}
		return new RefactoringStatus();
	}

	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		if (pm != null) {
			pm.beginTask("", 1); //$NON-NLS-1$
			pm.worked(1);
			pm.done();
		}
		return fChange;
	}

	@Override
	public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {

		if (pm == null)
			pm= new NullProgressMonitor();

		if (fProjects.isEmpty() || fCleanUps.isEmpty()) {
			pm.beginTask("", 1); //$NON-NLS-1$
			pm.worked(1);
			pm.done();
			fChange= new NullChange();

			return new RefactoringStatus();
		}

		int cuCount= getCleanUpTargetsSize();

		RefactoringStatus result= new RefactoringStatus();

		ICleanUp[] cleanUps= getCleanUps();
		pm.beginTask("", cuCount * 2 * fCleanUps.size() + 4 * cleanUps.length); //$NON-NLS-1$
		try {
			DynamicValidationStateChange change= new DynamicValidationStateChange(getName());
			change.setSchedulingRule(getSchedulingRule());
			for (Entry<IJavaProject, List<CleanUpTarget>> entry : fProjects.entrySet()) {
				IJavaProject project= entry.getKey();
				List<CleanUpTarget> targetsList= entry.getValue();
				CleanUpTarget[] targets= targetsList.toArray(new CleanUpTarget[targetsList.size()]);
				if (fUseOptionsFromProfile) {
					result.merge(setOptionsFromProfile(project, cleanUps));
					if (result.hasFatalError())
						return result;
				}
				result.merge(checkPreConditions(project, targets, new SubProgressMonitor(pm, 3 * cleanUps.length)));
				if (result.hasFatalError())
					return result;
				Change[] changes= cleanUpProject(project, targets, cleanUps, pm);
				result.merge(checkPostConditions(new SubProgressMonitor(pm, cleanUps.length)));
				if (result.hasFatalError())
					return result;
				for (Change c : changes) {
					change.add(c);
				}
			}
			fChange= change;

			List<IResource> files= new ArrayList<>();
			findFilesToBeModified(change, files);
			result.merge(Checks.validateModifiesFiles(files.toArray(new IFile[files.size()]), getValidationContext(), pm));
		} finally {
			pm.done();
		}

		return result;
	}

	private void findFilesToBeModified(CompositeChange change, List<IResource> result) throws JavaModelException {
		for (Change child : change.getChildren()) {
			if (child instanceof CompositeChange) {
				findFilesToBeModified((CompositeChange)child, result);
			} else if (child instanceof MultiStateCompilationUnitChange) {
				result.add(((MultiStateCompilationUnitChange)child).getCompilationUnit().getCorrespondingResource());
			} else if (child instanceof CompilationUnitChange) {
				result.add(((CompilationUnitChange)child).getCompilationUnit().getCorrespondingResource());
			}
		}
	}

	private Change[] cleanUpProject(IJavaProject project, CleanUpTarget[] targets, ICleanUp[] cleanUps, IProgressMonitor monitor) throws CoreException {
		CleanUpFixpointIterator iter= new CleanUpFixpointIterator(targets, cleanUps);

		SubProgressMonitor subMonitor= new SubProgressMonitor(monitor, 2 * targets.length * cleanUps.length);
		subMonitor.beginTask("", targets.length); //$NON-NLS-1$
		subMonitor.subTask(Messages.format(FixMessages.CleanUpRefactoring_Parser_Startup_message, BasicElementLabels.getResourceName(project.getProject())));
		try {
			while (iter.hasNext()) {
				iter.next(subMonitor);
			}

			return iter.getResult();
		} finally {
			iter.dispose();
			subMonitor.done();
		}
	}

	private RefactoringStatus setOptionsFromProfile(IJavaProject javaProject, ICleanUp[] cleanUps) {
		Map<String, String> options= CleanUpPreferenceUtil.loadOptions(new ProjectScope(javaProject.getProject()));
		if (options == null)
			return RefactoringStatus.createFatalErrorStatus(Messages.format(FixMessages.CleanUpRefactoring_could_not_retrive_profile, BasicElementLabels.getResourceName(javaProject.getProject())));

		CleanUpOptions cleanUpOptions= new MapCleanUpOptions(options);
		for (ICleanUp cleanUp : cleanUps) {
			cleanUp.setOptions(cleanUpOptions);
		}

		return new RefactoringStatus();
	}

	private RefactoringStatus checkPreConditions(IJavaProject javaProject, CleanUpTarget[] targets, IProgressMonitor monitor) throws CoreException {
		RefactoringStatus result= new RefactoringStatus();

		ICompilationUnit[] compilationUnits= new ICompilationUnit[targets.length];
		for (int i= 0; i < targets.length; i++) {
			compilationUnits[i]= targets[i].getCompilationUnit();
		}

		ICleanUp[] cleanUps= getCleanUps();
		monitor.beginTask("", compilationUnits.length * cleanUps.length); //$NON-NLS-1$
		monitor.subTask(Messages.format(FixMessages.CleanUpRefactoring_Initialize_message, BasicElementLabels.getResourceName(javaProject.getProject())));
		try {
			for (ICleanUp cleanUp : cleanUps) {
				result.merge(cleanUp.checkPreConditions(javaProject, compilationUnits, new SubProgressMonitor(monitor, compilationUnits.length)));
				if (result.hasFatalError())
					return result;
			}
		} finally {
			monitor.done();
		}

		return result;
	}

	private RefactoringStatus checkPostConditions(SubProgressMonitor monitor) throws CoreException {
		RefactoringStatus result= new RefactoringStatus();

		ICleanUp[] cleanUps= getCleanUps();
		monitor.beginTask("", cleanUps.length); //$NON-NLS-1$
		monitor.subTask(FixMessages.CleanUpRefactoring_checkingPostConditions_message);
		try {
			for (ICleanUp cleanUp : cleanUps) {
				result.merge(cleanUp.checkPostConditions(new SubProgressMonitor(monitor, 1)));
			}
		} finally {
			monitor.done();
		}
		return result;
	}

	private static String getChangeName(ICompilationUnit compilationUnit) {
		StringBuffer buf= new StringBuffer();
		JavaElementLabels.getCompilationUnitLabel(compilationUnit, JavaElementLabels.ALL_DEFAULT, buf);
		buf.append(JavaElementLabels.CONCAT_STRING);

		StringBuffer buf2= new StringBuffer();
		JavaElementLabels.getPackageFragmentLabel((IPackageFragment)compilationUnit.getParent(), JavaElementLabels.P_QUALIFIED, buf2);
		buf.append(buf2.toString().replace('.', '/'));

		return buf.toString();
	}

	public static CleanUpChange calculateChange(CleanUpContext context, ICleanUp[] cleanUps, List<ICleanUp> undoneCleanUps, HashSet<ICleanUp> slowCleanUps) throws CoreException {
		if (cleanUps.length == 0)
			return null;

		CleanUpChange solution= null;
		int i= 0;
		do {
			ICleanUp cleanUp= cleanUps[i];
			ICleanUpFix fix;
			if (slowCleanUps != null) {
				long timeBefore= System.currentTimeMillis();
				fix= cleanUp.createFix(context);
				if (System.currentTimeMillis() - timeBefore > SLOW_CLEAN_UP_THRESHOLD)
					slowCleanUps.add(cleanUp);
			} else {
				fix= cleanUp.createFix(context);
			}
			if (fix != null) {
				CompilationUnitChange current= fix.createChange(null);
				TextEdit currentEdit= current.getEdit();

				if (solution != null) {
					if (TextEditUtil.overlaps(currentEdit, solution.getEdit())) {
						undoneCleanUps.add(cleanUp);
					} else {
						CleanUpChange merge= new CleanUpChange(FixMessages.CleanUpRefactoring_clean_up_multi_chang_name, context.getCompilationUnit());
						merge.setEdit(TextEditUtil.merge(currentEdit, solution.getEdit()));

						copyChangeGroups(merge, solution);
						copyChangeGroups(merge, current);

						solution= merge;
					}
				} else {
					solution= new CleanUpChange(current.getName(), context.getCompilationUnit());
					solution.setEdit(currentEdit);

					copyChangeGroups(solution, current);
				}
			}
			i++;
		} while (i < cleanUps.length && (context.getAST() == null || !cleanUps[i].getRequirements().requiresFreshAST()));

		for (; i < cleanUps.length; i++) {
			undoneCleanUps.add(cleanUps[i]);
		}
		return solution;
	}

	private static void copyChangeGroups(CompilationUnitChange target, CompilationUnitChange source) {
		for (TextEditBasedChangeGroup changeGroup : source.getChangeGroups()) {
			TextEditGroup textEditGroup= changeGroup.getTextEditGroup();
			TextEditGroup newGroup;
			if (textEditGroup instanceof CategorizedTextEditGroup) {
				String label= textEditGroup.getName();
				newGroup= new CategorizedTextEditGroup(label, new GroupCategorySet(new GroupCategory(label, label, label)));
			} else {
				newGroup= new TextEditGroup(textEditGroup.getName());
			}
			for (TextEdit textEdit : textEditGroup.getTextEdits()) {
				newGroup.addTextEdit(textEdit);
			}
			target.addTextEditGroup(newGroup);
		}
	}

	@Override
	protected RefactoringTickProvider doGetRefactoringTickProvider() {
		return CLEAN_UP_REFACTORING_TICK_PROVIDER;
	}

	@Override
	public ISchedulingRule getSchedulingRule() {
		return ResourcesPlugin.getWorkspace().getRoot();
	}

	public static ASTParser createCleanUpASTParser() {
		ASTParser result= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);

		result.setResolveBindings(true);
		result.setStatementsRecovery(IASTSharedValues.SHARED_AST_STATEMENT_RECOVERY);
		result.setBindingsRecovery(IASTSharedValues.SHARED_BINDING_RECOVERY);

		return result;
	}

}
