/*
 * (c) Copyright IBM Corp. 2000, 2001.
 * All Rights Reserved.
 */
package net.sourceforge.phpdt.internal.corext.textmanipulation;

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

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;

/**
 * A helper class to arrange <code>TextEdit</code>s into a tree to optimize
 * their execution.
 */
/* package */abstract class TextEditNode {

	/* package */TextEditNode fParent;

	/* package */List fChildren;

	/* package */TextEdit fEdit;

	/* package */static class DefaultNode extends TextEditNode {
		public DefaultNode(TextEdit edit) {
			super(edit);
		}
	}

	/* package */static class RootNode extends TextEditNode {
		private int fUndoIndex;

		public RootNode(int length) {
			super(new NopTextEdit(new TextRange(0, length)));
			fEdit.isSynthetic = true;
		}

		public boolean covers(TextEditNode node) {
			return true;
		}

		public UndoMemento performDo(TextBuffer buffer, IProgressMonitor pm)
				throws CoreException {
			DoRangeUpdater updater = new DoRangeUpdater();
			UndoMemento undo = new UndoMemento(TextBufferEditor.UNDO);
			try {
				buffer.registerUpdater(updater);
				performDo(buffer, updater, undo, pm);
			} finally {
				buffer.unregisterUpdater(updater);
				updater.setActiveNode(null);
			}
			return undo;
		}

		public UndoMemento performUndo(TextBuffer buffer, IProgressMonitor pm)
				throws CoreException {
			UndoRangeUpdater updater = new UndoRangeUpdater(this);
			UndoMemento undo = new UndoMemento(TextBufferEditor.REDO);
			try {
				buffer.registerUpdater(updater);
				performUndo(buffer, updater, undo, pm);
			} finally {
				buffer.unregisterUpdater(updater);
				updater.setActiveNode(null);
			}
			return undo;
		}

		protected void setUndoIndex(int index) {
			fUndoIndex = index;
		}

		protected int getUndoIndex() {
			return fUndoIndex;
		}
	}

	/* package */abstract static class AbstractMoveNode extends TextEditNode {
		private int state;

		private int fTargetIndex;

		private int fSourceIndex;

		private List fAffectedChildren;

		public AbstractMoveNode(TextEdit edit) {
			super(edit);
			reset();
		}

		protected abstract TextRange getSourceRange();

		protected abstract TextRange getTargetRange();

		protected abstract boolean isUpMove();

		protected boolean isDownMove() {
			return !isUpMove();
		}

		public boolean isMove() {
			return true;
		}

		protected void checkRange(DocumentEvent event) {
			TextRange range = getChildRange();
			int eventOffset = event.getOffset();
			int eventLength = event.getLength();
			int eventEnd = eventOffset + eventLength - 1;
			// "Edit changes text that lies outside its defined range"
			// Assert.isTrue(range.fOffset <= eventOffset && eventEnd <=
			// range.getInclusiveEnd());
		}

		protected boolean activeNodeChanged(int delta) {
			TextRange targetRange = getTargetRange();
			TextRange sourceRange = getSourceRange();
			switch (state) {
			case 0: // the move delete
				init();
				// Assert.isTrue(Math.abs(delta) == sourceRange.fLength);
				if (isUpMove()) {
					updateOffset(fAffectedChildren, delta);
					targetRange.fOffset += delta;
				}
				sourceRange.fLength = 0;
				state = 1;
				break;
			case 1:
				TextEditNode target = (TextEditNode) fParent.fChildren
						.get(fTargetIndex);
				TextEditNode source = (TextEditNode) fParent.fChildren
						.get(fSourceIndex);
				updateOffset(source.fChildren, targetRange.fOffset
						- sourceRange.fOffset);
				target.fChildren = source.fChildren;
				if (target.fChildren != null) {
					for (Iterator iter = target.fChildren.iterator(); iter
							.hasNext();) {
						((TextEditNode) iter.next()).fParent = target;
					}
				}
				source.fChildren = null;
				if (isDownMove()) {
					updateOffset(fAffectedChildren, delta);
					sourceRange.fOffset += delta;
				}
				targetRange.fLength = delta;
				reset();
				break;
			}
			return true;
		}

		private static void updateOffset(List nodes, int delta) {
			if (nodes == null)
				return;
			for (int i = nodes.size() - 1; i >= 0; i--) {
				TextEditNode node = (TextEditNode) nodes.get(i);
				TextRange range = node.getTextRange();
				range.fOffset += delta;
				updateOffset(node.fChildren, delta);
			}
		}

		private void init() {
			TextRange source = getSourceRange();
			TextRange target = getTargetRange();
			List children = fParent.fChildren;
			for (int i = children.size() - 1; i >= 0; i--) {
				TextEditNode child = (TextEditNode) children.get(i);
				TextRange range = child.fEdit.getTextRange();
				if (range == source)
					fSourceIndex = i;
				else if (range == target)
					fTargetIndex = i;
			}
			int start = Math.min(fTargetIndex, fSourceIndex);
			int end = Math.max(fTargetIndex, fSourceIndex);
			fAffectedChildren = new ArrayList(3);
			for (int i = start + 1; i < end; i++) {
				fAffectedChildren.add(children.get(i));
			}
		}

		private void reset() {
			state = 0;
			fSourceIndex = -1;
			fTargetIndex = -1;
		}
	}

	/* package */static class MoveNode extends AbstractMoveNode {
		public MoveNode(TextEdit edit) {
			super(edit);
		}

		protected TextRange getChildRange() {
			return ((MoveTextEdit) fEdit).getChildRange();
		}

		protected TextRange getSourceRange() {
			return ((MoveTextEdit) fEdit).getSourceRange();
		}

		protected TextRange getTargetRange() {
			return ((MoveTextEdit) fEdit).getTargetRange();
		}

		protected boolean isUpMove() {
			return ((MoveTextEdit) fEdit).isUpMove();
		}

		public boolean isMovePartner(TextEditNode other) {
			if (!(other instanceof TargetMarkNode))
				return false;
			return fEdit == ((MoveTextEdit.TargetMark) other.fEdit)
					.getMoveTextEdit();
		}

		public boolean covers(TextEditNode node) {
			if (node instanceof TargetMarkNode) {
				MoveTextEdit.TargetMark edit = (MoveTextEdit.TargetMark) node.fEdit;
				if (edit.getMoveTextEdit() == fEdit)
					return false;
			}
			return getParentRange().covers(node.getChildRange());
		}
	}

	/* package */static class TargetMarkNode extends AbstractMoveNode {
		public TargetMarkNode(TextEdit edit) {
			super(edit);
		}

		protected TextRange getChildRange() {
			return ((MoveTextEdit.TargetMark) fEdit).getMoveTextEdit()
					.getChildRange();
		}

		protected TextRange getSourceRange() {
			return ((MoveTextEdit.TargetMark) fEdit).getMoveTextEdit()
					.getSourceRange();
		}

		protected TextRange getTargetRange() {
			return ((MoveTextEdit.TargetMark) fEdit).getMoveTextEdit()
					.getTargetRange();
		}

		protected boolean isUpMove() {
			return ((MoveTextEdit.TargetMark) fEdit).getMoveTextEdit()
					.isUpMove();
		}

		public boolean isMovePartner(TextEditNode other) {
			return ((MoveTextEdit.TargetMark) fEdit).getMoveTextEdit() == other.fEdit;
		}
	}

	// ---- Range updating
	// ---------------------------------------------------------------------------

	private static abstract class RangeUpdater implements IDocumentListener {
		protected TextEditNode fActiveNode;

		public void documentAboutToBeChanged(DocumentEvent event) {
		}

		public void setActiveNode(TextEditNode node) {
			fActiveNode = node;
		}

		public void updateParents(int delta) {
			TextEditNode node = fActiveNode.fParent;
			while (node != null) {
				node.childNodeChanged(delta);
				node = node.fParent;
			}
		}

		public static int getDelta(DocumentEvent event) {
			return (event.getText() == null ? 0 : event.getText().length())
					- event.getLength();
		}
	}

	private static class DoRangeUpdater extends RangeUpdater {
		private List fProcessedNodes = new ArrayList(10);

		public void setActiveNode(TextEditNode node) {
			if (fActiveNode != null)
				fProcessedNodes.add(fActiveNode);
			super.setActiveNode(node);
		}

		public void documentChanged(DocumentEvent event) {
			fActiveNode.checkRange(event);
			int delta = getDelta(event);
			if (!fActiveNode.activeNodeChanged(delta)) {
				for (Iterator iter = fProcessedNodes.iterator(); iter.hasNext();) {
					((TextEditNode) iter.next()).previousNodeChanged(delta);
				}
			}
			updateParents(delta);
		}
	}

	private static class UndoRangeUpdater extends RangeUpdater {
		private RootNode fRootNode;

		public UndoRangeUpdater(RootNode root) {
			fRootNode = root;
		}

		public void setActiveNode(TextEditNode node) {
			super.setActiveNode(node);
		}

		public void documentChanged(DocumentEvent event) {
			fActiveNode.checkRange(event);
			int delta = getDelta(event);
			if (!fActiveNode.activeNodeChanged(delta)) {
				int start = fRootNode.getUndoIndex() + 1;
				List children = fRootNode.fChildren;
				int size = children != null ? children.size() : 0;
				for (int i = start; i < size; i++) {
					updateUndo((TextEditNode) children.get(i), delta);
				}
			}
			updateParents(delta);
		}

		private void updateUndo(TextEditNode node, int delta) {
			node.previousNodeChanged(delta);
			List children = node.fChildren;
			int size = children != null ? children.size() : 0;
			for (int i = 0; i < size; i++) {
				updateUndo((TextEditNode) children.get(i), delta);
			}
		}
	}

	// ---- Creating instances
	// ---------------------------------------------------------------------------

	static TextEditNode create(TextEdit edit) {
		if (edit instanceof MoveTextEdit)
			return new MoveNode(edit);
		if (edit instanceof MoveTextEdit.TargetMark)
			return new TargetMarkNode(edit);
		return new DefaultNode(edit);
	}

	static RootNode createRoot(int length) {
		return new RootNode(length);
	}

	private TextEditNode(TextEdit edit) {
		fEdit = edit;
	}

	// ---- Adding children
	// ---------------------------------------------------------------------------

	protected void add(TextEditNode node) {
		if (fChildren == null) {
			fChildren = new ArrayList(1);
			node.fParent = this;
			fChildren.add(node);
			return;
		}
		// Optimize using binary search
		for (Iterator iter = fChildren.iterator(); iter.hasNext();) {
			TextEditNode child = (TextEditNode) iter.next();
			if (child.covers(node)) {
				child.add(node);
				return;
			}
		}
		for (int i = 0; i < fChildren.size();) {
			TextEditNode child = (TextEditNode) fChildren.get(i);
			if (node.covers(child)) {
				fChildren.remove(i);
				node.add(child);
			} else {
				i++;
			}
		}
		node.fParent = this;
		fChildren.add(node);
	}

	public boolean covers(TextEditNode node) {
		return false;
	}

	// ---- Accessing
	// --------------------------------------------------------------------------------------

	protected RootNode getRoot() {
		TextEditNode candidate = this;
		while (candidate.fParent != null)
			candidate = candidate.fParent;
		return (RootNode) candidate;
	}

	// ---- Query interface
	// --------------------------------------------------------------------------------

	protected boolean isSynthetic() {
		return fEdit.isSynthetic;
	}

	public boolean isMove() {
		return false;
	}

	// ---- Accessing Ranges
	// ------------------------------------------------------------------------------

	protected void checkRange(DocumentEvent event) {
		TextRange range = getTextRange();
		int eventOffset = event.getOffset();
		int eventLength = event.getLength();
		int eventEnd = eventOffset + eventLength - 1;
		// "Edit changes text that lies outside its defined range"
		// Assert.isTrue(range.fOffset <= eventOffset && eventEnd <=
		// range.getInclusiveEnd());
	}

	protected TextRange getTextRange() {
		return fEdit.getTextRange();
	}

	protected TextRange getChildRange() {
		return getTextRange();
	}

	protected TextRange getParentRange() {
		return getTextRange();
	}

	public boolean validate(int bufferLength) {
		if (fChildren == null)
			return true;
		// Only Moves and Nops can be parents
		if (!(fEdit instanceof MoveTextEdit || fEdit instanceof NopTextEdit))
			return false;
		TextRange lastRange = null;
		for (Iterator iter = fChildren.iterator(); iter.hasNext();) {
			TextEditNode node = (TextEditNode) iter.next();
			if (!node.validate(bufferLength))
				return false;
			TextRange range = node.fEdit.getTextRange();
			if (!range.isValid()
					|| range.fOffset + range.fLength > bufferLength)
				return false;
			if (lastRange != null
					&& !(range.isInsertionPointAt(lastRange.fOffset) || range
							.liesBehind(lastRange)))
				return false;
			lastRange = range;
		}
		return true;
	}

	// ---- Updating
	// ----------------------------------------------------------------------------------------

	protected boolean activeNodeChanged(int delta) {
		TextRange range = getTextRange();
		range.fLength += delta;
		// we didn't adjust any processed nodes.
		return false;
	}

	protected void previousNodeChanged(int delta) {
		TextRange range = getTextRange();
		range.fOffset += delta;
	}

	protected void childNodeChanged(int delta) {
		getTextRange().fLength += delta;
	}

	// ---- Do it
	// ---------------------------------------------------------------------------------------------

	protected void performDo(TextBuffer buffer, RangeUpdater updater,
			UndoMemento undo, IProgressMonitor pm) throws CoreException {
		int size = fChildren != null ? fChildren.size() : 0;
		for (int i = size - 1; i >= 0; i--) {
			TextEditNode child = (TextEditNode) fChildren.get(i);
			child.performDo(buffer, updater, undo, pm);
		}
		updater.setActiveNode(this);
		if (isSynthetic())
			fEdit.perform(buffer);
		else
			undo.add(fEdit.perform(buffer));
		pm.worked(1);
	}

	public void performedDo() {
		int size = fChildren != null ? fChildren.size() : 0;
		for (int i = size - 1; i >= 0; i--) {
			TextEditNode child = (TextEditNode) fChildren.get(i);
			child.performedDo();
		}
		fEdit.performed();
	}

	// ---- Undo it
	// -------------------------------------------------------------------------------------------

	protected void performUndo(TextBuffer buffer, RangeUpdater updater,
			UndoMemento undo, IProgressMonitor pm) throws CoreException {
		int size = fChildren != null ? fChildren.size() : 0;
		for (int i = 0; i < size; i++) {
			setUndoIndex(i);
			TextEditNode child = (TextEditNode) fChildren.get(i);
			child.performUndo(buffer, updater, undo, pm);
		}
		updater.setActiveNode(this);
		if (isSynthetic())
			fEdit.perform(buffer);
		else
			undo.add(fEdit.perform(buffer));
		pm.worked(1);
	}

	protected void setUndoIndex(int index) {
	}

	public void performedUndo() {
		int size = fChildren != null ? fChildren.size() : 0;
		for (int i = 0; i < size; i++) {
			TextEditNode child = (TextEditNode) fChildren.get(i);
			child.performedUndo();
		}
		fEdit.performed();
	}

	// protected void createUndoList(List list) {
	// int size= fChildren != null ? fChildren.size() : 0;
	// for (int i= 0; i < size; i++) {
	// TextEditNode child= (TextEditNode)fChildren.get(i);
	// child.createUndoList(list);
	// }
	// list.add(this);
	// }
}
