package org.jcon.ui.tree;

import org.jcon.util.TNode;
import java.awt.Component;
import java.awt.event.*;
import java.util.Vector;

import javax.swing.DefaultCellEditor;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.TreeCellEditor;

import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

/**
 * A wrapper providing an easy-to-use JTree in a Panel.
 * Another benefit is clients need not import JTree and
 * associated classes. "B" stands for Better.
 *
 * @author Jack Harich
 */
public class BTree implements // extends JPanel
    FocusListener, KeyListener, DocumentListener,
    CellEditorListener, TreeSelectionListener,
    MouseListener {

//---------- Private Fields ------------------------------
private JTree                tree = new JTree();
private JScrollPane          scrollPane = new JScrollPane();
private TNode                rootNode;
private JTextField           textFieldEditor = new JTextField();
private String               beforeEditText;
private ActionListener       actionListener;
private BTreeCellEditor cellEditor = new BTreeCellEditor(this); 
private Vector               listeners = new Vector();

//---------- Initialization -----------------------------
public BTree() {   
    // Single selection only
    tree.getSelectionModel().setSelectionMode(
        TreeSelectionModel.SINGLE_TREE_SELECTION);
    ((DefaultTreeModel)tree.getModel()).setAsksAllowsChildren(true);
    
    // Set to react rationally to clicking elsewhere after edit
    tree.setInvokesStopCellEditing(true);
    
    tree.addTreeSelectionListener(this);
    tree.addMouseListener(this);
    tree.addKeyListener(this);
    
    scrollPane.getViewport().add(tree);
}

  public void setModel(DefaultTreeModel model) {
    tree.setModel(model);
  }

//---------- Superclass Overrides ------------------------
// Must setRootNode before adding listeners
// MouseListener
public void addMouseListener(MouseListener listener) {
  print("---\n\n adding " + listener + " as mouse listener");
    if (tree!= null) tree.addMouseListener(listener);
}
public void removeMouseListener(MouseListener listener) {
    if (tree!= null) tree.removeMouseListener(listener);
}
// KeyListener
public void addKeyListener(KeyListener listener) {
    if (tree!= null) tree.addKeyListener(listener);
}
public void removeKeyListener(KeyListener listener) {
    if (tree!= null) tree.removeKeyListener(listener);
}
//---------- FocusListener Inmplementation ---------------
public void focusGained(FocusEvent evt) {
    beforeEditText = textFieldEditor.getText();
}
public void focusLost(FocusEvent evt) {
    // *** ???
}
//---------- KeyListener Implementation ------------------
public void keyTyped(KeyEvent evt) { }
public void keyPressed(KeyEvent evt) {

// *** Swing bug - Events not reliable
//print(".keyPresssed() " + evt.getKeyCode() + ", VK_ENTER = " + KeyEvent.VK_ENTER);

    if (evt.getKeyCode() == KeyEvent.VK_ESCAPE
            && evt.getSource() == textFieldEditor) {
        //print(".keyPressed() - ESC");
        textFieldEditor.setText(beforeEditText);
        tree.stopEditing(); // *** Not implemented ???
        
    } else if (evt.getKeyCode() == KeyEvent.VK_ENTER
            && evt.getSource() == tree) {
        if (getSelectedNode() != null) {
            fireEvent(BTreeEvent.NODE_ACTIVATED);
        }
    }
}
public void keyReleased(KeyEvent evt) { }

//---------- DocumentListener Implementation -------------
// *** Still buggy, getting false insert and remove events,
// no changed events. Use odd method of saving initial state
public void insertUpdate(DocumentEvent evt) {
    //print(".insertUpdate()");
}
public void removeUpdate(DocumentEvent evt) {
    //print(".removeUpdate()");
}
public void changedUpdate(DocumentEvent evt) {
    //print(".changedUpdate()");
}
//---------- CellEditorListener Implementation -----------
public void editingCanceled(ChangeEvent evt) {
    fireActionEvent("EditingCanceled");
}
public void editingStopped(ChangeEvent evt) {
    fireActionEvent("EditingStopped");
        
    if (! beforeEditText.equals(textFieldEditor.getText())) {
        fireActionEvent("ValueChanged");
    }
}
private void fireActionEvent(String command) {
    if (actionListener != null) {
        ActionEvent event = new ActionEvent(this,
            ActionEvent.ACTION_PERFORMED, command);
        actionListener.actionPerformed(event);
    }
}
//---------- TreeSelectionListener Implementation --------
public void valueChanged(TreeSelectionEvent evt) {
    fireEvent(BTreeEvent.NODE_SELECTED);
}
//---------- MouseListener Implementation ----------------
public void mouseClicked(MouseEvent evt) {
    //print(".mouseClicked()");
    
}
public void mousePressed(MouseEvent evt) {
    //print(".mousePressed()");
    
}
public void mouseReleased(MouseEvent evt) {
    //print(".mouseReleased()");
    // Most reliable on Win95
    if (evt.getClickCount() == 2) {
        fireEvent(BTreeEvent.NODE_ACTIVATED);
    }
}
public void mouseEntered(MouseEvent evt) { }
public void mouseExited(MouseEvent evt) { }
//---------- Properties ----------------------------------
//----- rootNode
/**
 * Sets the all important rootNode for the tree, and
 * initializes this class. Therefore this method MUST be
 * called before working with this class. Generally it will
 * be the first method called.
 */
public void setRootNode(TNode rootNode) {
    this.rootNode = rootNode;
    tree.setModel(new DefaultTreeModel(rootNode));
}    
public TNode getRootNode() {
    return rootNode;
}
//----- isEditable
// *** Design flaw - must setRootNode() first
public void setEditable(boolean editable) {
    tree.setEditable(editable);
    if (editable) { // *** Need to only call ONCE
        textFieldEditor.addFocusListener(this);
        textFieldEditor.addKeyListener(this);
        textFieldEditor.getDocument().addDocumentListener(this);

        // Left is left of icon, right is correct,
        // No slow click to edit, right double click to edit
        cellEditor.addCellEditorListener(this);
        tree.setCellEditor(cellEditor);

    } else {
        tree.setCellEditor(null);
    }
}
public boolean isEditable() {
    return tree.isEditable();
}
//----- RootVisible
public void setRootVisible(boolean visible) {
    tree.setRootVisible(visible);
}
public boolean isRootVisible() {
    return tree.isRootVisible();
}
//----- Other
public Component getComponent() {
    return (Component)scrollPane;
}
/**
* Use this to add popups
*/
public JComponent getContentPane() {
    return (JComponent)tree;
}
// *** MOD to not expose this, use event ***************
public TreeSelectionModel getSelectionModel() {
    return tree.getSelectionModel();
}
/**
 * Returns null if no node selected.
 */
public Object getSelectedUserObject() {
    TNode currentNode = getSelectedNode();
    if (currentNode == null) {
        return null;
    } else {
        return currentNode.getUserObject();
    }
}
/**
 * Returns null if no node selected.
 */
public TNode getSelectedNode() {
    TreePath path = tree.getSelectionPath();
    if (path == null) {
        return null;
    } else {
        return (TNode)path.getLastPathComponent();
    }
}
/**
* Sets the listener for action events such as
* "ValueChanged". (*** under design) We take a simple
* approach here instead of a custom event.
*/
public void setActionListener(ActionListener listener) {
    actionListener = listener;
}    
/**
 * Sets the cell renderer. This is optional, since a very
 * good default is provided using the userObject.toString()
 * and standard expanded and collapsed folders.
 */
public void setCellRenderer(TreeCellRenderer renderer) {
    tree.setCellRenderer(renderer);
}
public void setRowHeight(int height) {
    tree.setRowHeight(height);
}
public int getRowHeight() {
    return tree.getRowHeight();
}    
//---------- Events --------------------------------------
public void addBTreeListener(BTreeListener listener) {
    listeners.addElement(listener);
}
public void removeBTreeListener(BTreeListener listener) {
    listeners.removeElement(listener);
}
//---------- Public Methods ------------------------------
public void selectSingleNode(TNode node) {
    tree.clearSelection();
    TreePath treePath = new TreePath(node.getPath());
    tree.setSelectionPath(treePath);
}
/**
 * Causes the tree to refresh all its nodes. Currently this
 * unfortunately collapses the tree to 2 levels.
 */
public void refresh() {
    nodeStructureChanged(rootNode);
    //tree.treeDidChange(); // NO EFFECT ************

    // This was the favorite:
    //((DefaultTreeModel)tree.getModel()).reload();
//    tree.updateUI();
    // But tree.updateUI() also collapses the tree

    // *** Need a way to show tree as it was expanded.
    // reload(TreeNode) called automatically in Apply
    // is best, but reload(TreeNode) is "Pending".
}
/**
* Call this when the contents of the currently selected
* node have changed. Has no effect if no selected node.
*/
public void currentNodeChanged() {
    TNode currentNode = getSelectedNode();
    if (currentNode != null) nodeChanged(currentNode);
}
/**
 * Call this when the contents of a node have changed.
 */
public void nodeChanged(TNode node) {
    ((DefaultTreeModel)tree.getModel()).nodeChanged(node);
}
/**
 * Call this when the children (or their children) of a
 * node have changed, such as adding or removing one.
 */
public void nodeStructureChanged(TNode node) {
    ((DefaultTreeModel)tree.getModel()).nodeStructureChanged(node);
    // *** Apparent bug if custom renderer
    // But adding this line causes collapse in TestBTree
    //SwingUtilities.updateComponentTreeUI(tree);
}
/**
 * Adds the popup for future use.
 */
public void addPopup(JPopupMenu popupMenu) {
    tree.add(popupMenu); // *** was this.add
}
/**
 * Returns true if MouseEvent occurred on a row, false if
 * it occurred on empty space. This is used to avoid
 * displaying a popup when the user doesn't click on a row.
 */
public boolean rowIsBeneathMouse(MouseEvent evt) {
    int index = tree.getRowForLocation(evt.getX(), evt.getY());
    return (index != -1 ? true : false);
}
/**
 * Shows the popup at the MouseEvent x and y point.
 */
public void showPopup(JPopupMenu popup, MouseEvent evt) {
    //print(".showPopup() " + evt.getX() + " " + evt.getY());
    popup.show(tree, evt.getX(), evt.getY() );
}
// *** Mod to not expose this **************************
public void addTreeSelectionListener(TreeSelectionListener listener) {
    tree.addTreeSelectionListener(listener);
}
public void openChildren() {
    //int[] rows = tree.getSelectionRows();
    //if (rows.length > 0) tree.expandRow(rows[0]);
    TreePath treePath = tree.getSelectionPath();
    tree.expandPath(treePath);
}
public void openBranch() {
    TreePath treePath = tree.getSelectionPath();
    DefaultMutableTreeNode node =
        (DefaultMutableTreeNode)treePath.getLastPathComponent();
    openNode(node);
}
// RECURSIVE *** public??? Called by openBranch()
public void openNode(DefaultMutableTreeNode node) {
    // Expand this node
    TreeNode[] treeNodes = node.getPath();
    TreePath treePath = new TreePath(treeNodes);
    tree.expandPath(treePath);
    // Open children recursively
    for (int i = 0; i < node.getChildCount(); i++) {
        DefaultMutableTreeNode childNode =
            (DefaultMutableTreeNode)node.getChildAt(i);
        if (! childNode.isLeaf()) openNode(childNode);
    }
}
/* public void resortChildren(TNode parentNode) {
    // Get children
    Vector children = new Vector();
    int count = parentNode.getChildCount();
    for (int i = 0; i < count; i++) {
        children.addElement(parentNode.getChildAt(i));
    }
    // Remove children
    parentNode.removeAllChildren();
    // Add children
    for (int i = 0; i < count; i++) {
        TNode child = (TNode)children.elementAt(i);
        parentNode.insertAlphabetically(child);
    }
} */
//---------- Package Methods -----------------------------
JTextField getTextFieldEditor() {
    return textFieldEditor;
}
//---------- Private Methods -----------------------------
private void fireEvent(int eventType) {
    BTreeEvent evt = new BTreeEvent(eventType);
    evt.setNode(getSelectedNode());

    //    print("firing event: ");
    //    dumpStack();
    
    Vector list;
    synchronized(this) {
        list = (Vector)listeners.clone();
    }
    for (int i = 0; i < list.size(); i++) {
        BTreeListener listener = (BTreeListener)list.elementAt(i);
        listener.processBTreeEvent(evt);
    }
}    
  public static void dumpStack() {
    Thread.currentThread().dumpStack();
  }

//--- Std
private static void print(String text) {
    System.out.println("BTree" + text);
}

} // End class
