/*******************************************************************************
 * Copyright (c) 2007, 2012 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
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.dom.rewrite.changegenerator;

import org.eclipse.text.edits.CopyTargetEdit;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MoveSourceEdit;
import org.eclipse.text.edits.MoveTargetEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;

public class TextEditUtil {
	// Do not instantiate. All methods are static.
	private TextEditUtil() {
	}

	/**
	 * Degenerates the given edit tree into a list.<br>
	 * All nodes of the result are leafs.<br>
	 * <strong>The given edit is modified and can no longer be used.</strong>
	 *
	 * @param edit the edit tree to flatten
	 * @return a MultiTextEdit containing the list of edits
	 */
	public static MultiTextEdit flatten(TextEdit edit) {
		MultiTextEdit result = new MultiTextEdit();
		flatten(edit, result);
		return result;
	}

	private static void flatten(TextEdit edit, MultiTextEdit result) {
		if (edit.hasChildren()) {
			TextEdit[] children = edit.getChildren();
			for (int i = 0; i < children.length; i++) {
				TextEdit child = children[i];
				child.getParent().removeChild(0);
				flatten(child, result);
			}
		} else if (!(edit instanceof MultiTextEdit)) {
			result.addChild(edit);
		}
	}

	/**
	 * Create an edit which contains <code>edit1</code> and <code>edit2</code>
	 * <p><strong>The given edits are modified and they can no longer be used.</strong></p>
	 *
	 * @param edit1 the edit to merge with edit2
	 * @param edit2 the edit to merge with edit1
	 * @return the merged tree
	 * @throws MalformedTreeException if the two edits overlap
	 */
	public static TextEdit merge(TextEdit edit1, TextEdit edit2) {
		if (edit1 instanceof MultiTextEdit && !edit1.hasChildren()) {
			return edit2;
		}

		if (edit2 instanceof MultiTextEdit && !edit2.hasChildren()) {
			return edit1;
		}

		MultiTextEdit result = new MultiTextEdit();
		merge(edit1, edit2, result);
		return result;
	}

	private static void merge(TextEdit edit1, TextEdit edit2, MultiTextEdit result) {
		if (edit1 instanceof MultiTextEdit && edit2 instanceof MultiTextEdit) {
			MultiTextEdit multiTextEdit1 = (MultiTextEdit) edit1;
			if (!multiTextEdit1.hasChildren()) {
				result.addChild(edit2);
				return;
			}

			MultiTextEdit multiTextEdit2 = (MultiTextEdit) edit2;
			if (!multiTextEdit2.hasChildren()) {
				result.addChild(edit1);
				return;
			}

			TextEdit[] children1 = multiTextEdit1.getChildren();
			TextEdit[] children2 = multiTextEdit2.getChildren();

			int i1 = 0;
			int i2 = 0;
			while (i1 < children1.length && i2 < children2.length) {
				while (i1 < children1.length && children1[i1].getExclusiveEnd() < children2[i2].getOffset()) {
					edit1.removeChild(0);
					result.addChild(children1[i1]);
					i1++;
				}
				if (i1 >= children1.length)
					break;

				while (i2 < children2.length && children2[i2].getExclusiveEnd() < children1[i1].getOffset()) {
					edit2.removeChild(0);
					result.addChild(children2[i2]);
					i2++;
				}
				if (i2 >= children2.length)
					break;

				if (children1[i1].getExclusiveEnd() < children2[i2].getOffset())
					continue;

				edit1.removeChild(0);
				edit2.removeChild(0);
				merge(children1[i1], children2[i2], result);

				i1++;
				i2++;
			}

			while (i1 < children1.length) {
				edit1.removeChild(0);
				result.addChild(children1[i1]);
				i1++;
			}

			while (i2 < children2.length) {
				edit2.removeChild(0);
				result.addChild(children2[i2]);
				i2++;
			}
		} else if (edit1 instanceof MultiTextEdit) {
			TextEdit[] children = edit1.getChildren();

			int i = 0;
			while (children[i].getExclusiveEnd() < edit2.getOffset()) {
				edit1.removeChild(0);
				result.addChild(children[i]);
				i++;
				if (i >= children.length) {
					result.addChild(edit2);
					return;
				}
			}
			edit1.removeChild(0);
			merge(children[i], edit2, result);
			i++;
			while (i < children.length) {
				edit1.removeChild(0);
				result.addChild(children[i]);
				i++;
			}
		} else if (edit2 instanceof MultiTextEdit) {
			TextEdit[] children = edit2.getChildren();

			int i = 0;
			while (children[i].getExclusiveEnd() < edit1.getOffset()) {
				edit2.removeChild(0);
				result.addChild(children[i]);
				i++;
				if (i >= children.length) {
					result.addChild(edit1);
					return;
				}
			}
			edit2.removeChild(0);
			merge(edit1, children[i], result);
			i++;
			while (i < children.length) {
				edit2.removeChild(0);
				result.addChild(children[i]);
				i++;
			}
		} else {
			if (edit1.getExclusiveEnd() < edit2.getOffset()) {
				result.addChild(edit1);
				result.addChild(edit2);
			} else {
				result.addChild(edit2);
				result.addChild(edit1);
			}
		}
	}

	/**
	 * Returns the difference in the document length caused by the edit. {@code InsertEdit}s have
	 * positive delta, {@code DeleteEdit}s have negative one.
	 * @param edit the edit to determine delta for.
	 * @return the delta
	 */
	public static int delta(TextEdit edit) {
		int delta = 0;
		for (TextEdit child : edit.getChildren()) {
			delta += delta(child);
		}
		delta += ownDelta(edit);
		return delta;
	}

	private static int ownDelta(TextEdit edit) {
		if (edit instanceof DeleteEdit || edit instanceof MoveSourceEdit) {
			return -edit.getLength();
		} else if (edit instanceof InsertEdit) {
			return ((InsertEdit) edit).getText().length();
		} else if (edit instanceof ReplaceEdit) {
			return ((ReplaceEdit) edit).getText().length() - edit.getLength();
		} else if (edit instanceof CopyTargetEdit) {
			return ((CopyTargetEdit) edit).getSourceEdit().getLength();
		} else if (edit instanceof MoveTargetEdit) {
			return ((MoveTargetEdit) edit).getSourceEdit().getLength();
		}
		return 0;
	}
}
