package org.autoplot.jythonsupport.ui;

import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.undo.*;


/*
**  This class will merge individual edits into a single larger edit.
**  That is, characters entered sequentially will be grouped together and
**  undone as a group. Any attribute changes will be considered as part
**  of the group and will therefore be undone when the group is undone.
*/
public class CompoundUndoManager extends UndoManager
        implements UndoableEditListener, DocumentListener {

    private UndoManager undoManager;
    private CompoundEdit compoundEdit;
    private JTextComponent textComponent;
    private UndoAction undoAction;
    private RedoAction redoAction;

    //  These fields are used to help determine whether the edit is an
    //  incremental edit. The offset and length should increase by 1 for
    //  each character added or decrease by 1 for each character removed.
    private int lastOffset;
    private int lastLength;

    public CompoundUndoManager(JTextComponent textComponent) {
        this.textComponent = textComponent;
        undoManager = this;
        undoAction = new UndoAction();
        redoAction = new RedoAction();
        textComponent.getDocument().addUndoableEditListener(this);
    }

    /*
	**  Add a DocumentLister before the undo is done so we can position
	**  the Caret correctly as each edit is undone.
     */
    public void undo() {
        textComponent.getDocument().addDocumentListener(this);
        super.undo();
        textComponent.getDocument().removeDocumentListener(this);
    }

    /*
	**  Add a DocumentLister before the redo is done so we can position
	**  the Caret correctly as each edit is redone.
     */
    public void redo() {
        textComponent.getDocument().addDocumentListener(this);
        super.redo();
        textComponent.getDocument().removeDocumentListener(this);
    }

    /*
	**  Whenever an UndoableEdit happens the edit will either be absorbed
	**  by the current compound edit or a new compound edit will be started
     */
    public void undoableEditHappened(UndoableEditEvent e) {
        //  Start a new compound edit

        if (compoundEdit == null) {
            compoundEdit = startCompoundEdit(e.getEdit());
            return;
        }

        int offsetChange = textComponent.getCaretPosition() - lastOffset;
        int lengthChange = textComponent.getDocument().getLength() - lastLength;

        if ( e.getEdit() instanceof AbstractDocument.DefaultDocumentEvent ) {
            //  Check for an attribute change
            AbstractDocument.DefaultDocumentEvent event
                    = (AbstractDocument.DefaultDocumentEvent) e.getEdit();

            if (event.getType().equals(DocumentEvent.EventType.CHANGE)) {
                if (offsetChange == 0) {
                    compoundEdit.addEdit(e.getEdit());
                    return;
                }
            }
        } else { // if ( e.getEdit() instanceof javax.swing.text.AbstractDocument$DefaultDocumentEventUndoableWrapper ) {
            //System.err.println( "086:" + e.getEdit().getUndoPresentationName().equals("Undo addition") ) ;
            if ( e.getEdit().getUndoPresentationName().equals("Undo addition") ) { // TODO:  THIS DOES NOT WORK...
                if (offsetChange == 0) {
                    compoundEdit.addEdit(e.getEdit());
                    return;
                }
            }
        }

        //  Check for an incremental edit or backspace.
        //  The Change in Caret position and Document length should both be
        //  either 1 or -1.
//		int offsetChange = textComponent.getCaretPosition() - lastOffset;
//		int lengthChange = textComponent.getDocument().getLength() - lastLength;
        if (offsetChange == lengthChange
                && Math.abs(offsetChange) == 1) {
            compoundEdit.addEdit(e.getEdit());
            lastOffset = textComponent.getCaretPosition();
            lastLength = textComponent.getDocument().getLength();
            return;
        }

        //  Not incremental edit, end previous edit and start a new one
        compoundEdit.end();
        compoundEdit = startCompoundEdit(e.getEdit());
    }

    /*
	**  Each CompoundEdit will store a group of related incremental edits
	**  (ie. each character typed or backspaced is an incremental edit)
     */
    private CompoundEdit startCompoundEdit(UndoableEdit anEdit) {
        //  Track Caret and Document information of this compound edit

        lastOffset = textComponent.getCaretPosition();
        lastLength = textComponent.getDocument().getLength();

        //  The compound edit is used to store incremental edits
        compoundEdit = new MyCompoundEdit();
        compoundEdit.addEdit(anEdit);

        //  The compound edit is added to the UndoManager. All incremental
        //  edits stored in the compound edit will be undone/redone at once
        addEdit(compoundEdit);

        undoAction.updateUndoState();
        redoAction.updateRedoState();

        return compoundEdit;
    }

    /*
	 *  The Action to Undo changes to the Document.
	 *  The state of the Action is managed by the CompoundUndoManager
     */
    public Action getUndoAction() {
        return undoAction;
    }

    /*
	 *  The Action to Redo changes to the Document.
	 *  The state of the Action is managed by the CompoundUndoManager
     */
    public Action getRedoAction() {
        return redoAction;
    }
//
//  Implement DocumentListener
//

    /*
	 *  Updates to the Document as a result of Undo/Redo will cause the
	 *  Caret to be repositioned
     */
    public void insertUpdate(final DocumentEvent e) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                int offset = e.getOffset() + e.getLength();
                offset = Math.min(offset, textComponent.getDocument().getLength());
                textComponent.setCaretPosition(offset);
            }
        });
    }

    public void removeUpdate(DocumentEvent e) {
        textComponent.setCaretPosition(e.getOffset());
    }

    public void changedUpdate(DocumentEvent e) {
    }

    class MyCompoundEdit extends CompoundEdit {

        public boolean isInProgress() {
            //  in order for the canUndo() and canRedo() methods to work
            //  assume that the compound edit is never in progress

            return false;
        }

        public void undo() throws CannotUndoException {
            //  End the edit so future edits don't get absorbed by this edit

            if (compoundEdit != null) {
                compoundEdit.end();
            }

            super.undo();

            //  Always start a new compound edit after an undo
            compoundEdit = null;
        }
    }

    /*
	 *	Perform the Undo and update the state of the undo/redo Actions
     */
    class UndoAction extends AbstractAction {

        public UndoAction() {
            putValue(Action.NAME, "Undo");
            putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
            putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_U));
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control Z"));
            setEnabled(false);
        }

        public void actionPerformed(ActionEvent e) {
            try {
                undoManager.undo();
                textComponent.requestFocusInWindow();
            } catch (CannotUndoException ex) {
            }

            updateUndoState();
            redoAction.updateRedoState();
        }

        private void updateUndoState() {
            setEnabled(undoManager.canUndo());
        }
    }

    /*
	 *	Perform the Redo and update the state of the undo/redo Actions
     */
    class RedoAction extends AbstractAction {

        public RedoAction() {
            putValue(Action.NAME, "Redo");
            putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
            putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_R));
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK));
            setEnabled(false);
        }

        public void actionPerformed(ActionEvent e) {
            try {
                undoManager.redo();
                textComponent.requestFocusInWindow();
            } catch (CannotRedoException ex) {
            }

            updateRedoState();
            undoAction.updateUndoState();
        }

        protected void updateRedoState() {
            setEnabled(undoManager.canRedo());
        }
    }
}