package org.jcon.ui.texteditor;

import org.jcon.ui.VisualLib;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Label;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.text.JTextComponent;

import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

/**
 * This class is a reusable text editor, with functionality
 * similar to any simple editor. It is designed to added 
 * to a window as the main component, though it can easily
 * be a minor one.
 *<p>
 * The prime feature is undo/redo, which users expect these
 * days. It also has various hot keys. It also has a toolbar
 * so the user can see the undo/redo availability.
 * See class UnitTest for a use example.
 * <p> 
 * The features and hot keys are: <p> <pre>
 * - Undo/Redo for last 100 edits
 * - Toolbar with Undo and Redo buttons. Can add more.
 * - Tabs are converted to 4 spaces (Swing BUG, not working)
 * - Dirty state is maintained as a property (needs improvement)
 * - TextEditEvents are available
 * - Ctrl Z - Undo
 * - Ctrl R - Redo
 * - Ctrl HOME - To document top
 * - Ctrl END - To document bottom </pre>
 * <p>
 * We expect to add features such as exposing action items
 * for menu use, find, etc.
 * 
 * @author Jack Harich
 */
 
// *** To do:
// - Get reliable dirty using undo subsystem (hard) 

public class TextEditor implements UndoableEditListener,
    DocumentListener, KeyListener {

//---------- Private Fields ------------------------------
private JTextComponent editor;
private JScrollPane    editorScroller;
private JToolBar       toolbar;
private Vector         listeners;
private boolean        isDirty;
private boolean        isNoEvents;

private UndoManager    undoManager;
private UndoAction     undoAction; // Inner class
private RedoAction     redoAction; // Inner class

private JButton        undoBtn;
private JButton        redoBtn;

//---------- Initialization ------------------------------
public TextEditor() { 
    listeners = new Vector();
          
    // Undo feature
    undoManager = new UndoManager();
    //undoManager.setLimit(5); // ***********TEST
    undoAction = new UndoAction();
    redoAction = new RedoAction();
    
    undoBtn = createActionButton("Undo", undoAction,
        "Undoes the last edit - Control Z");
    redoBtn = createActionButton("Redo", redoAction,
        "Redoes the last edit that was undone - Control R");
    
    // Prepare toolBar. This may be property driven later.
    toolbar = new JToolBar();
    toolbar.add(undoBtn);
    toolbar.add(redoBtn);
    
    // Put textArea in scroller
    editor = new JTextArea();
    editor.setBackground(Color.white);
    editor.setDoubleBuffered(true); // *** Appears not to work yet
    editor.setFont(new Font("Courier", Font.PLAIN, 12));
    // *** setTabSize() - Leaves tab in text but shows as spaces
    // This is NOT what we need. Setting to zero causes bug
    //((JTextArea)editor).setTabSize(0); 
    editor.getDocument().addUndoableEditListener(this);
    editor.getDocument().addDocumentListener(this);
    editor.addKeyListener(this); // For hot keys
    
    editorScroller = new JScrollPane();
    editorScroller.getViewport().add(editor);
    editorScroller.getViewport().setBackingStoreEnabled(true); // Doesn't seem to spped up yet
}
//---------- UndoableEditListener Implementation ---------
public void undoableEditHappened(UndoableEditEvent evt) {
    //print(".undoableEditHappened() - " + evt.getEdit().getPresentationName());
    undoManager.addEdit(evt.getEdit());
    undoAction.update();
    redoAction.update();
}
//---------- DocumentListener Implementation -------------  
public void insertUpdate(DocumentEvent evt) {
    setDirtyState(true);
    fireEvent(new TextEditorEvent(TextEditorEvent.CHANGED));   
}
public void removeUpdate(DocumentEvent evt) { 
    setDirtyState(true);    
    fireEvent(new TextEditorEvent(TextEditorEvent.CHANGED));   
}
public void changedUpdate(DocumentEvent evt) {
    // Ignore poorly named method. Better is attributeChanged()
} 
//----- Move...
// Inserts 4 spaces at current caret position  
private void insertSpaces() {
    int caretPosition = editor.getCaretPosition();
    try {
        editor.getDocument().insertString(
            caretPosition, "    ", null); // 4 spaces
    } catch(Exception ex) {
        ex.printStackTrace(); // Should not happen   
    }
}    
//---------- KeyListener Implementation ------------------
public void keyPressed(KeyEvent evt) {
    //print("pressed evt.getKeyCode() = " + evt.getKeyCode() );    
    int keyCode = evt.getKeyCode();
    
    // Convert tab to spaces
    if (keyCode == KeyEvent.VK_TAB) {
        evt.consume();
        insertSpaces();
        return;   
    }
    // Hot keys for basic functionality
    if (! evt.isControlDown()) return;
        
    if (keyCode == KeyEvent.VK_Z) {
        undoAction.perform();
        
    } else if (keyCode == KeyEvent.VK_R) {
        redoAction.perform();
        
    } else if (keyCode == KeyEvent.VK_HOME) {
        editor.setCaretPosition(0);

    } else if (keyCode == KeyEvent.VK_END) {
        int length = editor.getText().length();
        editor.setCaretPosition(length);
    }    
}
public void keyReleased(KeyEvent evt) { }
public void keyTyped(KeyEvent evt) { }

//---------- Properties ----------------------------------
/**
* Returns the toolbar, which already has an Undo and Redo
* button. More may be added.
*/
public JToolBar getToolBar() {
    return toolbar;
}
/**
* Returns the main component, which is the editor in a
* scroller.
*/
public Component getMainComponent() {
    return (Component)editorScroller;   
}    
//----- text
/**
* Sets the text for the editor and flushes Undo/Redo.
* This method does NOT cause events to be fired.
* This is extremely useful in keeping client code simple.
*/
public void setText(String text) {
    isNoEvents = true;
        editor.setText(text);
        editor.setCaretPosition(0); // Swing bug fix, was at end
        setDirty(false);
    isNoEvents = false;        
}   
public String getText() {
    return editor.getText();
}
//----- dirty
/**
* Sets the document dirty state. Currently this may only
* be set to false. This causes no events.
*/
public void setDirty(boolean dirty) {
    if (dirty) throw new IllegalArgumentException("Setting to dirty = true not supported.");
    isNoEvents = true;
        undoManager.discardAllEdits();
        undoAction.update();
        redoAction.update();    
        setDirtyState(false);
    isNoEvents = false;  
}
public boolean isDirty() {
    return isDirty;   
}  
//----- enabled
public void setEnabled(boolean enabled) {
    editor.setEnabled(enabled);    
}      
public boolean isEnabled() {
    return editor.isEnabled();   
}  
//----- editable
public void setEditable(boolean editable) {
    editor.setEditable(editable);
    if (editable) {
        editor.setBackground(Color.white);
    } else {
        editor.setBackground(Color.lightGray);
    }
}  
public boolean isEditable() {
    return editor.isEditable();
}
//---------- Events --------------------------------------
public void addTextEditorListener(TextEditorListener listener) {
    listeners.addElement(listener);   
}
public void removeTextEditorListener(TextEditorListener listener) {
    listeners.removeElement(listener);   
}   
//---------- Public Methods ------------------------------

//---------- Private Methods -----------------------------
private void setDirtyState(boolean newState) {
    if (isDirty == newState) return;
    
    isDirty = newState;
    //print(".setDirtyState() - dirty changed to " + isDirty);
    
    if (isNoEvents) return; // Optimization
    if (isDirty) {
        fireEvent(new TextEditorEvent(TextEditorEvent.TO_DIRTY));
    } else {
        fireEvent(new TextEditorEvent(TextEditorEvent.TO_CLEAN));        
    } 
}    
private void fireEvent(TextEditorEvent evt) {
    if (isNoEvents) return;
        
    Vector list;
    synchronized(this) {
        list = (Vector)listeners.clone();   
    }  
    for (int i = listeners.size(); --i >= 0; ) {
        TextEditorListener listener = (TextEditorListener)listeners.elementAt(i);
        listener.textEditorEvented(evt);            
    }    
}
private JButton createActionButton(String text,
        Action action, String toolTip) {
            
    String command = (String)action.getValue(Action.NAME);
    JButton button = VisualLib.createCompactButton(
        text, command, toolTip);
    button.addActionListener(action);
    action.addPropertyChangeListener(new ActionManager(button));    
    button.setEnabled(action.isEnabled()); 
    button.setForeground(Color.blue);
       
    return button;
} 
//--- Std
private static void print(String text) {
    System.out.println("TextEditor" + text);
}
//========== Inner Classes ===============================
// Handles button enablement, maybe more later such as
// menu item string maintenance like in Notepad example
private class ActionManager implements PropertyChangeListener {
    private Component comp;
    
    ActionManager(Component component) {
        comp = component;
    }
    //---------- PropertyChangeListener Implementation
    public void propertyChange(PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals("enabled")) {
            boolean enabled = ((Boolean)evt.getNewValue()).booleanValue();
            comp.setEnabled(enabled);
        }
    }
}    
private class UndoAction extends AbstractAction {
    public UndoAction() {
        super("Undo");
        this.setEnabled(false);
    }
    public void actionPerformed(ActionEvent evt) {
        try {
            if(undoManager.canUndo()) undoManager.undo();
        } catch (CannotUndoException ex) {
            print(" - UndoAction - Unable to undo: " + ex);
            ex.printStackTrace();
        }
        update();
        redoAction.update();
    }
    // Logically dirty if enabled <-----<<<
    // Fire DIRTY_CHANGED when enabled changes
    // UNRELIABLE if UndoManager limit reached. Cannot
    // figure out how to handle this. Swing is poorly designed.
    protected void update() {
        if(undoManager.canUndo()) {
            if (! this.isEnabled()) {
                this.setEnabled(true);
                // ***setDirtyState(true);
            }
            putValue(Action.NAME, undoManager.getUndoPresentationName());
        } else {
            if (this.isEnabled()) {
                this.setEnabled(false);
                // ***setDirtyState(false);
            }
            putValue(Action.NAME, "Undo");
        }
    }
    protected void perform() {
        if (this.isEnabled()) actionPerformed(null);
    }
} // End inner class
    
private class RedoAction extends AbstractAction {
    public RedoAction() {
        super("Redo");
        this.setEnabled(false);
    }
    public void actionPerformed(ActionEvent evt) {
        try {
            if(undoManager.canRedo()) undoManager.redo();
        } catch (CannotRedoException ex) {
            System.out.println("Unable to redo: " + ex);
            ex.printStackTrace();
        }
        update();
        undoAction.update();
    }
    protected void update() {
        if(undoManager.canRedo()) {
            this.setEnabled(true);
            putValue(Action.NAME, undoManager.getRedoPresentationName());
        } else {
            this.setEnabled(false);
            putValue(Action.NAME, "Redo");
        }
    }
    protected void perform() {
        if (this.isEnabled()) actionPerformed(null);
    }    
} // End inner class
        
} // End outer class
