/*
 * 12/06/2008
 *
 * RUndoManager.java - Handles undo/redo behavior for RTextArea.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rtextarea;

import java.util.ResourceBundle;
import javax.swing.Action;
import javax.swing.UIManager;
import javax.swing.event.UndoableEditEvent;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;


/**
 * This class manages undos/redos for a particular editor pane.  It groups
 * all undos that occur one character position apart together, to avoid
 * Java's horrible "one character at a time" undo behavior.  It also
 * recognizes "replace" actions (i.e., text is selected, then the user
 * types), and treats it as a single action, instead of a remove/insert
 * action pair.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class RUndoManager extends UndoManager {

	private RCompoundEdit compoundEdit;
	private RTextArea textArea;
	private int lastOffset;
	private String cantUndoText;
	private String cantRedoText;

	private int internalAtomicEditDepth;

	private static final String MSG	= "org.fife.ui.rtextarea.RTextArea";


	/**
	 * Constructor.
	 *
	 * @param textArea The parent text area.
	 */
	public RUndoManager(RTextArea textArea) {
		this.textArea = textArea;
		ResourceBundle msg = ResourceBundle.getBundle(MSG);
		cantUndoText = msg.getString("Action.CantUndo.Name");
		cantRedoText = msg.getString("Action.CantRedo.Name");
	}


	/**
	 * Begins an "atomic" edit.  This method is called when RTextArea
	 * KNOWS that some edits should be compound automatically, such as
	 * when the user is typing in overwrite mode (the deletion of the
	 * current char + insertion of the new one) or the playing back of a
	 * macro.
	 *
	 * @see #endInternalAtomicEdit()
	 */
	public void beginInternalAtomicEdit() {
		if (++internalAtomicEditDepth==1) {
			if (compoundEdit!=null)
				compoundEdit.end();
			compoundEdit = new RCompoundEdit();
		}
	}


	/**
	 * Ends an "atomic" edit.
	 *
	 * @see #beginInternalAtomicEdit()
	 */
	public void endInternalAtomicEdit() {
		if (internalAtomicEditDepth>0 && --internalAtomicEditDepth==0) {
			addEdit(compoundEdit);
			compoundEdit.end();
			compoundEdit = null;
			updateActions();	// Needed to show the new display name.
		}
	}


	/**
	 * Returns the localized "Can't Redo" string.
	 *
	 * @return The localized "Can't Redo" string.
	 * @see #getCantUndoText()
	 */
	public String getCantRedoText() {
		return cantRedoText;
	}


	/**
	 * Returns the localized "Can't Undo" string.
	 *
	 * @return The localized "Can't Undo" string.
	 * @see #getCantRedoText()
	 */
	public String getCantUndoText() {
		return cantUndoText;
	}


	/**
	 * {@inheritDoc}
	 */
	@Override
	public void redo() throws CannotRedoException {
		super.redo();
		updateActions();
	}


	private RCompoundEdit startCompoundEdit(UndoableEdit edit) {
		lastOffset = textArea.getCaretPosition();
		compoundEdit = new RCompoundEdit();
		compoundEdit.addEdit(edit);
		addEdit(compoundEdit);
		return compoundEdit;
	}


	/**
	 * {@inheritDoc}
	 */
	@Override
	public void undo() throws CannotUndoException {
		super.undo();
		updateActions();
	}


	@Override
	public void undoableEditHappened(UndoableEditEvent e) {

		// This happens when the first undoable edit occurs, and
		// just after an undo.  So, we need to update our actions.
		if (compoundEdit==null) {
			compoundEdit = startCompoundEdit(e.getEdit());
			updateActions();
			return;
		}

		else if (internalAtomicEditDepth>0) {
			compoundEdit.addEdit(e.getEdit());
			return;
		}

		// This happens when there's already an undo that has occurred.
		// Test to see if these undos are on back-to-back characters,
		// and if they are, group them as a single edit.  Since an
		// undo has already occurred, there is no need to update our
		// actions here.
		int diff = textArea.getCaretPosition() - lastOffset;
		// "<=1" allows contiguous "overwrite mode" key presses to be
		// grouped together.
		if (Math.abs(diff)<=1) {//==1) {
			compoundEdit.addEdit(e.getEdit());
			lastOffset += diff;
			//updateActions();
			return;
		}

		// This happens when this UndoableEdit didn't occur at the
		// character just after the previous undlabeledit.  Since an
		// undo has already occurred, there is no need to update our
		// actions here either.
		compoundEdit.end();
		compoundEdit = startCompoundEdit(e.getEdit());
		//updateActions();

	}


	/**
	 * Ensures that undo/redo actions are enabled appropriately and have
	 * descriptive text at all times.
	 */
	public void updateActions() {

		String text;

		Action a = RTextArea.getAction(RTextArea.UNDO_ACTION);
		if (canUndo()) {
			a.setEnabled(true);
			text = getUndoPresentationName();
			a.putValue(Action.NAME, text);
			a.putValue(Action.SHORT_DESCRIPTION, text);
		}
		else {
			if (a.isEnabled()) {
				a.setEnabled(false);
				text = cantUndoText;
				a.putValue(Action.NAME, text);
				a.putValue(Action.SHORT_DESCRIPTION, text);
			}
		}

		a = RTextArea.getAction(RTextArea.REDO_ACTION);
		if (canRedo()) {
			a.setEnabled(true);
			text = getRedoPresentationName();
			a.putValue(Action.NAME, text);
			a.putValue(Action.SHORT_DESCRIPTION, text);
		}
		else {
			if (a.isEnabled()) {
				a.setEnabled(false);
				text = cantRedoText;
				a.putValue(Action.NAME, text);
				a.putValue(Action.SHORT_DESCRIPTION, text);
			}
		}

	}

	/**
	 * The edit used by {@link RUndoManager}.
	 */
	class RCompoundEdit extends CompoundEdit {

		@Override
		public String getUndoPresentationName() {
			return UIManager.getString("AbstractUndoableEdit.undoText");
		}

		@Override
		public String getRedoPresentationName() {
			return UIManager.getString("AbstractUndoableEdit.redoText");
		}

		@Override
		public boolean isInProgress() {
			return false;
		}

		@Override
		public void undo() throws CannotUndoException {
			if (compoundEdit!=null)
				compoundEdit.end();
			super.undo();
			compoundEdit = null;
		}

	}


}