/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * The Original Code is the Grendel mail/news client.
 *
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation.  Portions created by Netscape are
 * Copyright (C) 1997 Netscape Communications Corporation. All
 * Rights Reserved.
 *
 * Contributor(s): 
 *
 * Created: Will Scullin <scullin@netscape.com>, 21 Aug 1997.
 * Modified: Jeff Galyan <jeffrey.galyan@sun.com>, 30 Dec 1998
 */

package grendel.widgets;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;

import java.util.Enumeration;
import java.util.Vector;

import javax.swing.event.CellEditorListener;
import javax.swing.CellRendererPane;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicGraphicsUtils;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;

import grendel.dnd.DropTarget;
import grendel.dnd.DropTargetComponent;
import grendel.dnd.DragSource;

/**
 * A outliner view similar to those used in previous versions of
 * mail and news.
 */

public class TreeTable extends JComponent implements Scrollable,
                                                     DropTargetComponent
{
  // For synchronization
  TreeTable           fTreeTable;

  CellRendererPane    fCellRendererPane = new CellRendererPane();
  ColumnModel         fColumnModel;
  ColumnHeader        fColumnHeader;
  TreeTableDataModel  fDataModel;
  int                 fHeight;
  TreeNode            fRoot;
  TreeModelListener   fDataModelListener = new TreeModelListener();

  // Selection/focus
  TreeNode            fCaret;
  TreeNode            fAnchor;
  SelectionManager    fSelection = new MultiSelectionManager();
  int                 fHitX, fHitY;
  Point               fHitPoint;
  boolean             fDragging = false;
  Point               fTooltipPoint = null;

  // Editing
  boolean             fEditing = false;
  CellEditor          fEditor;
  TreeNode            fEditedNode;
  Column              fEditedColumn;
  boolean             fMousinessAfoot = false;
  boolean             fJustSelected = false;
  DropTarget          fDropTarget = null;

  // Attributes
  Object              fTreeColumn;
  int                 fIndentLevel = 16;
  int                 fRowHeight = 0;
  int                 fRowMargin = 0;
  boolean             fFixed = true;
  static final int    kGap = 4;

  // Properties
  boolean             fDrawPipes = false;

  // Images
  Icon                fPlusIcon;
  Icon                fMinusIcon;

  Color               fWindowColor;
  Color               fHighlightColor;

  //
  // Constructors
  //

  /**
   * Constructs a TreeTable with the given data model.
   */

  public TreeTable(TreeTableDataModel aDataModel) {
    fTreeTable = this;

    setLayout(null);
    setFont(new Font("Helvetica", Font.PLAIN, 12));
    add(fCellRendererPane);

    fColumnModel = new ColumnModelImp();
    fColumnModel.addColumnModelListener(new TreeColumnModelListener());
    fColumnHeader = new ColumnHeader(fColumnModel);

    TreeMouseListener mouseListener = new TreeMouseListener();
    addMouseListener(mouseListener);
    addMouseMotionListener(mouseListener);
    addFocusListener(new TreeFocusListener());

    fSelection = new MultiSelectionManager();
    fSelection.addSelectionListener(new TreeSelectionListener());

    updateUI();

    initializeKeys();

    if (aDataModel != null) {
      updateDataModel(aDataModel);
    }
  }

  //
  // Accessor functions
  //

  /**
   * Sets the current data model. A null value (should be)
   * acceptable.
   */

  public synchronized void setDataModel(TreeTableDataModel aDataModel) {
    if (fDataModel != null) {
      clearDataModel();
    }
    updateDataModel(aDataModel);

    resizeAndRepaint();
  }

  /**
   * Returns the current data model.
   *
   * @see TreeTableDataModel
   */

  public synchronized TreeTableDataModel getDataModel() {
     return fDataModel;
  }

  /**
   * Retuns the column model
   *
   * @see ColumnModel
   */

  public synchronized ColumnModel getColumnModel() {
    return fColumnModel;
  }

  /**
   * Adds a column to the tree table. The column is added at the end.
   */

  public void addColumn(Column aColumn) {
    fColumnModel.addColumn(aColumn);
  }

  /**
   * Adds a column at the given position. Existing columns may be
   * shifted to the right.
   */

  public void addColumnAt(Column aColumn, int aIndex) {
    fColumnModel.addColumnAt(aColumn, aIndex);
  }

  /**
   * Returns the ColumnHeader widget that allows the user to
   * view and manipulate column properties.
   */

  public ColumnHeader getColumnHeader() {
    return fColumnHeader;
  }

  /**
   * Returns the last object on which the user clicked. This
   * object is used for keyboard navigation.
   */

  public TreePath getCaret() {
    return buildTreePath(fCaret);
  }

  void setCaret(TreeNode aNode) {
    if (fCaret != null) {
      repaint(fCaret);
    }
    fCaret = aNode;
    if (fCaret != null) {
      repaint(fCaret);
    }
  }

  /**
   * Sets the last object on which the user clicked. This
   * object is used for keyboard navigation.
   */

  public void setCaret(TreePath aPath) {
    TreeNode node = findTreeNode(aPath);
    if (node != null) {
      setCaret(node);
    }
  }

  /**
   * Returns the interface for the tree table's selection manager
   */

  public SelectionManager getSelectionManager() {
    return fSelection;
  }

  //
  // Attributes
  //

  /**
   * Sets which column in which the tree indention and (eventually)
   * icons appear.
   */

  public void setTreeColumn(Object aID) {
    fTreeColumn = aID;
  }

  /**
   * Returns which column in which the tree indention and (eventually)
   * icons appear.
   */

  public Object getTreeColumn() {
    return fTreeColumn;
  }

  /**
   * Sets the number of pixels each level of the tree is indented
   * (16 or more is currently recommended.)
   */

  public void setIndentLevel(int aIndent) {
    fIndentLevel = aIndent;
  }

  /**
   * Returns the number of pixels each level of the tree is indented.
   */

  public int getIndentLevel() {
    return fIndentLevel;
  }

  /**
   * Sets a fixed row height. Overrides the per-row sizing feature.
   * A height of 0 enables the per-row sizing feature.
   */

  public void setRowHeight(int aHeight) {
    fRowHeight = aHeight;
  }

  /**
   * Returns the fixed row height
   * @see setRowHeight()
   */

  public int getRowHeight() {
    return fRowHeight;
  }

  /**
   * Sets whether all rows have the same height. Saves a tremendous
   * amount of time if true. Defaults to true.
   * @see isFixedHeight()
   */

  public void setFixedHeight(boolean aFixed) {
    fFixed = aFixed;
  }

  /**
   * Returns whether all rows have the same height. Saves a tremendous
   * amount of time if true.
   * @see setFixedHeight()
   */

  public boolean isFixedHeight() {
    return fFixed;
  }

  /**
   * Does nothing, yet.
   */

  public void setRowMargin(int aMargin) {
    fRowMargin = aMargin;
  }

  /**
   * Does nothing, yet.
   */

  public int getRowMargin() {
    return fRowMargin;
  }

  //
  // Selection functions
  //

  Enumeration getShiftRange(TreePath aPath) {
    if (fAnchor == null) {
      fAnchor = getVisibleRoot();
    }

    TreeNode node = findTreeNode(aPath);
    int anchorY = getNodeY(fAnchor);
    int nodeY = getNodeY(node);

    TreeNode first;
    TreeNode last;

    if (anchorY < nodeY) {
      first = fAnchor;
      last = node;
    } else {
      first = node;
      last = fAnchor;
    }

    Vector range = new Vector();
    boolean done = false;
    while (!done && first != null) {
      range.insertElementAt(buildTreePath(first), range.size());

      if (first == last) {
        done = true;
      } else {
        TreeNode next = null;

        if (!first.isCollapsed() && !first.isLeaf()) {
          first = first.getFirstChild();
        } else {
          next = first.getNextSibling();
          if (next == null) {
            TreeNode parent = first.getParent();
            while (next == null && parent != null) {
              next = parent.getNextSibling();
              parent = parent.getParent();
            }
          }
          first = next;
        }
      }
    }
    return range.elements();
  }

  void select(TreePath aPath, int aModifiers) {
    boolean shiftKey = (aModifiers & KeyEvent.SHIFT_MASK) != 0;
    boolean controlKey = (aModifiers & KeyEvent.CTRL_MASK) != 0;
    boolean rightMouse = (aModifiers & KeyEvent.BUTTON3_MASK) != 0;

    // Complex heuristic for selecting based on windows.
    // Probably wrong for other platforms.
    // Maybe we need a plugable UI after all.

    if (controlKey) {
      if (!rightMouse) {
        if (fSelection.isSelected(aPath)) {
          if (shiftKey) {
            fSelection.removeSelection(getShiftRange(aPath));
          } else {
            fSelection.removeSelection(aPath);
          }
        } else {
          if (shiftKey) {
            fSelection.addSelection(getShiftRange(aPath));
          } else {
            fSelection.addSelection(aPath);
          }
        }
      }
    } else {
      if (rightMouse) {
        if (!fSelection.isSelected(aPath)) {
          fSelection.setSelection(aPath);
        }
      } else {
        if (shiftKey) {
          fSelection.setSelection(getShiftRange(aPath));
        } else {
          fSelection.setSelection(aPath);
        }
      }
    }

    if (!shiftKey) {
      fAnchor = findTreeNode(aPath);
    }
  }

  /**
   * Selects the given TreePath.
   */

  public void select(TreePath aPath) {
    select(aPath, 0);
  }

  //
  // Navigation functions
  //

  /**
   * Navigation used for up arrow key. Does reverse in-order
   * traversal, skipping children of collapsed nodes.
   */

  void navigateUp(int aModifiers) {
    if (fCaret != null) {
      if (fCaret != getVisibleRoot()) {
        TreeNode prev, last;
        prev = fCaret.getPrevSibling();
        if (prev == null) {
          setCaret(fCaret.getParent());
        } else {
          setCaret((TreeNode) null);
          while (prev != null && fCaret == null) {
            if (!prev.isCollapsed() && prev.getFirstChild() != null) {
              prev = prev.getLastChild();
            } else {
              setCaret(prev);
            }
          }
        }
      }
    }

    if (fCaret == null) {
      setCaret(getVisibleRoot());
    }

    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }

  /**
   * Navigation used for down arrow key. Does in-order
   * traversal, skipping children of collapsed nodes.
   */

  void navigateDown(int aModifiers) {
    if (fCaret != null) {
      TreeNode next = null;

      if (!fCaret.isCollapsed()) {
        next = fCaret.getFirstChild();
      }

      if (next == null) {
        next = fCaret.getNextSibling();
        if (next == null) {
          TreeNode parent = fCaret.getParent();
          while (next == null && parent != null) {
            next = parent.getNextSibling();
            parent = parent.getParent();
          }
        }
      }

      if (next != null) {
        setCaret(next);
      }
    } else {
      setCaret(getVisibleRoot());
    }

    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }

  /**
   * Navigation used for left arrow key. First collapses node,
   * then moves to parent
   */

  void navigateLeft(int aModifiers) {
    if (fCaret != null) {
      if (fCaret.isLeaf() || fCaret.isCollapsed()) {
        fCaret = fCaret.getParent();
      } else {
        collapse(fCaret);
      }
    }

    if (fCaret == null) {
      setCaret(getVisibleRoot());
    }

    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }

  /**
   * Navigation used for right arrow key. First expands node,
   * then moves to first child
   */

  void navigateRight(int aModifiers) {
    if (fCaret != null) {
      if (!fCaret.isLeaf()) {
        if (!fCaret.isCollapsed()) {
          fCaret = fCaret.getFirstChild();
        } else {
          expand(fCaret);
        }
      }
    }

    if (fCaret == null) {
      setCaret(getVisibleRoot());
    }

    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }

  void navigateHome(int aModifiers) {
    setCaret(getVisibleRoot());
    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }

  TreeNode findLastVisible() {
    TreeNode node = getVisibleRoot();
    TreeNode res = null;

    while (node != null) {
      res = node;
      if (node.getNextSibling() != null) {
        node = node.getNextSibling();
      } else {
        node = node.getFirstChild();
      }
    }
    return res;
  }

  void navigateEnd(int aModifiers) {
    setCaret(findLastVisible());
    if (fCaret == null) {
      setCaret(getVisibleRoot());
    }
    select(buildTreePath(fCaret), aModifiers);
    ensureVisible(fCaret);
  }


  /**
   * Moves the caret up one position, and selects that object.
   * (currently doesn't support scrolling)
   */

  public void navigateUp() {
    navigateUp(0);
  }

  /**
   * Moves the caret down one position, and selects that object.
   * (currently doesn't support scrolling)
   */

  public void navigateDown() {
    navigateDown(0);
  }

  /**
   * Returns whether or not the give path is visible. If the path
   * points to the root, and the root is not shown, the method
   * still returns true.
   */

  public boolean isVisible(TreePath aPath) {
    return (findTreeNode(aPath) != null);
  }

  /**
   * Attempts to tweek parent into displaying the given node.
   */

  void ensureVisible(TreeNode aNode) {
    ensureVisible(buildTreePath(aNode));
  }

  public void ensureVisible(TreePath aPath) {
    int y = getNodeY(aPath);
    int h = getNodeHeight(aPath);

    if (y != -1) {
      Container parent = getParent();
      if (parent != null && parent instanceof JViewport) {
        JViewport parentView = (JViewport) parent;
        Dimension viewSize = parentView.getExtentSize();
        Point viewPos = parentView.getViewPosition();

        if (y < viewPos.y) {
          viewPos.y = y;
          parentView.setViewPosition(viewPos);
        } else if ((y + h) > (viewPos.y + viewSize.height)) {
          viewPos.y = y - viewSize.height + h;
          parentView.setViewPosition(viewPos);
        }
      }
    }
  }

  int getNodeY(TreeNode aNode) {
    return getNodeY(buildTreePath(aNode));
  }

  /**
   * Returns the Y location of the give node
   */

  public int getNodeY(TreePath aPath) {
    Object path[] = aPath.getPath();
    int length = aPath.getLength();
    int index = 0;
    int y = 0;
    TreeNode traverse = fRoot;

    if (!fDataModel.showRoot()) {
      index = 1;
      if (traverse != null) {
        traverse = traverse.getFirstChild();
      }
    }

    while (index < length) {
      while (traverse != null && traverse.getData() != path[index]) {
        y += traverse.getTotalHeight();
        traverse = traverse.getNextSibling();
      }

      if (traverse == null) {
        return -1;
      }

      index++;
      if (index < length) {
        y += traverse.getNodeHeight();
        traverse = traverse.getFirstChild();
      }
    }

    return y;
  }

  public int getNodeHeight(TreePath aPath) {
    if(fRowHeight > 0) {
      return fRowHeight;
    }

    TreeNode node = findTreeNode(aPath);
    if (node == null) {
      return -1;
    }
    return node.getNodeHeight();
  }

  public int getColumnX(Column column) {
    int res = -1;
    if (column != null) {
      int idx = fColumnModel.getColumnIndex(column);
      int margin = fColumnModel.getColumnMargin();

      res = 0;
      for (int i = 0; i < idx; i++) {
        res += fColumnModel.getColumn(i).getWidth() + margin;
      }
    }

    return res;
  }

  //
  // Utility functions
  //

  TreeNode getVisibleRoot() {
    if (fDataModel == null) {
      return null;
    }

    if (fDataModel.showRoot()) {
      return fRoot;
    } else {
      return fRoot.getFirstChild();
    }
  }

  TreePath buildTreePath(TreeNode aNode) {
    if (aNode == null) {
      return null;
    }

    int depth = aNode.getDepth();
    Object path[] = new Object[depth + 1];

    while(aNode != null) {
      path[depth] = aNode.getData();
      aNode = aNode.getParent();
      depth--;
    }

    return new TreePath(path);
  }

  TreeNode findTreeNodeRecurse(TreeNode aNode, Object aPath[], int aFirst, int aLast) {
    while (aNode != null) {
      if (aNode.getData() == aPath[aFirst]) {
        if (aFirst == aLast) {
          return aNode;
        } else {
          return findTreeNodeRecurse(aNode.getFirstChild(), aPath, aFirst + 1, aLast);
        }
      } else {
        aNode = aNode.getNextSibling();
      }
    }
    return null;
  }

  /**
   * Finds a TreeNode given a path. If null is returned, either
   * the path is invalid, or the path isn't fully visible.
   */

  synchronized TreeNode findTreeNode(TreePath aPath) {
    return findTreeNodeRecurse(fRoot, aPath.getPath(), 0, aPath.getLength() - 1);
  }

  synchronized void resizeAndRepaint() {
    setSize(getPreferredSize());
    repaint();
  }

  synchronized void resizeAndRepaintFrom(TreePath aPath) {
    setSize(getPreferredSize());
    repaintFrom(aPath);
  }

  synchronized void clearDataModel() {
    fSelection.clearSelection();
    setCaret((TreeNode) null);

    fDataModel.removeTreeTableModelListener(fDataModelListener);
    fDataModel = null;

    TreeNode node = fRoot;
    while (node != null) {
      TreeNode next = node.getNextSibling();
      node.detachSelf();
      node = next;
    }
    fRoot = null;
  }

  synchronized void updateDataModel(TreeTableDataModel aDataModel) {
    fDataModel = aDataModel;

    if (fDataModel != null) {
      fDataModel.addTreeTableModelListener(fDataModelListener);
    }

    reload();
  }

  /**
   * Forces the tree to rebuild itself from scratch.
   * Doesn't repaint.
   */

  public void reload() {
    if (fDataModel != null) {
      Enumeration children = fDataModel.getChildren(null);
      if (children != null) {
        if (children.hasMoreElements()) {
          // First case where children are given as enumerations
          Object node = children.nextElement();
          fRoot = new TreeNode(fDataModel, null, node);
          TreeNode prev = fRoot;

          while (children.hasMoreElements()) {
            node = children.nextElement();
            prev.addSiblingAfter(new TreeNode(fDataModel, null, node));
            prev = prev.getNextSibling();
          }
        } else {
          fRoot = null;
        }
      } else {
        // Second case where children are give by getFirstChild,
        // getNextSibling
        Object node = fDataModel.getRoot();
        if (node != null) {
          fRoot = new TreeNode(fDataModel, null, node);
          TreeNode prev = fRoot;

          node = fDataModel.getNextSibling(node);
          while (node != null) {
            prev.addSiblingAfter(new TreeNode(fDataModel, null, node));
            prev = prev.getNextSibling();
            node = fDataModel.getNextSibling(node);
          }
        } else {
          fRoot = null;
        }
      }
    }
    initializeCached();
  }

  void initializeCached() {
    TreeNode node = getVisibleRoot();
    fHeight = 0;

    while (node != null) {
      fHeight += node.getTotalHeight();
      node = node.getNextSibling();
    }
  }

  void initializeKeys() {
    registerKeyboardAction(new NavigateUpAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new NavigateDownAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new NavigateLeftAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new NavigateRightAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new NavigateHomeAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new NavigateEndAction(0),
                           KeyStroke.getKeyStroke(KeyEvent.VK_END, 0),
                           WHEN_FOCUSED);
    registerKeyboardAction(new OpenAction(),
                           KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
                           WHEN_FOCUSED);
  }

  void expand(TreeNode aNode) {
    fDataModel.setCollapsed(buildTreePath(aNode), false);
  }

  void collapse(TreeNode aNode) {
    fDataModel.setCollapsed(buildTreePath(aNode), true);
  }

  int getIndent(Column aColumn, TreeNode aNode) {
    int indent = 0;
    if (aColumn.getID().equals(fTreeColumn)) {
      Icon icon = fDataModel.getIcon(aNode.getData());
      indent = aNode.getVisibleDepth() * fIndentLevel + fIndentLevel +
        (icon != null ? icon.getIconWidth() : 0) + kGap;
    }
    return indent;
  }

  int drawPipes(Graphics g, TreeNode aNode, int x, int y, int w, int h) {
    TreeNode node = aNode;
    int depth = aNode.getVisibleDepth();
    x += depth * fIndentLevel;

    int midY = y + h / 2;

    g.setColor(Color.black);

    int iconWidth = 0;
    Icon icon = fDataModel.getIcon(aNode.getData());

    if (icon != null) {
      iconWidth = icon.getIconWidth();
      int iconY = midY - icon.getIconHeight() / 2;

      icon.paintIcon(this, g, x + fIndentLevel, iconY);

      Icon overlayIcon = fDataModel.getOverlayIcon(aNode.getData());
      if (overlayIcon != null) {
        overlayIcon.paintIcon(this, g, x + fIndentLevel, iconY);
      }
    }

    while (node != null && node.getVisibleDepth() >= 0) {
      int midX = x + fIndentLevel / 2;

      if (fDrawPipes) {
        if (node.getNextSibling() != null) {
          g.drawLine(midX, y, midX, y + h);
        }

        if (aNode == node) {
          g.drawLine(midX, midY, x + fIndentLevel, midY);
          if (node.getParent() != null) {
            g.drawLine(midX, midY, midX, y);
          }
        }
      }

      if (node == aNode && !node.isLeaf()) {
        if (node.isCollapsed()) {
          fPlusIcon.paintIcon(this, g, x, y);
        } else {
          fMinusIcon.paintIcon(this, g, x, y);
        }
      }

      node = node.getParent();
      x -= fIndentLevel;
    }

    return fIndentLevel + depth * fIndentLevel + iconWidth + kGap;
  }

  /**
   * Recursive part of <code>paint()</code>.
   * @see #paint
   */

  static int fPaintedRows;

  void paintRecurse(Graphics g, TreeNode aRoot, int y) {
    int x;
    int w, h;
    boolean selected = false;
    boolean done = false;
    TreeNode node = aRoot;

    int columnMargin = fColumnModel.getColumnMargin();

    Rectangle rect = g.getClipBounds();

    while (node != null && !done) {
      TreePath path = buildTreePath(node);

      h = node.getNodeHeight();
      x = 0;
      selected = fSelection.isSelected(path);

      if (y + h >= rect.y) { // Paint only visible stuff
        if (y < rect.y + rect.height) { // ditto

          fPaintedRows++;

          Enumeration columns = fColumnModel.getColumns();

          g.setColor(selected ? fHighlightColor : fWindowColor);
          g.fillRect(0, y, getWidth(), h);

          while (columns.hasMoreElements()) {
            Column column = (Column) columns.nextElement();
            CellRenderer renderer = column.getCellRenderer();
            w = column.getWidth();

            if (x + w >= rect.x && x < rect.x + rect.width) {
              renderer.setValue(node.getData(),
                                fDataModel.getData(node.getData(), column.getID()),
                                selected);

              Component component = renderer.getComponent();

              int dx = x;
              int dw = w;
              if (column.getID().equals(fTreeColumn)) {
                int pipeWidth = drawPipes(g, node, x, y, w, h);
                dx = x + pipeWidth;
                dw = w - pipeWidth;
              }

              fCellRendererPane.paintComponent(g, component, this, dx, y, dw, h);
            }

            x += w + columnMargin;
          }

          if (hasFocus() && node == fCaret) {
            g.setXORMode(Color.white);
            BasicGraphicsUtils.drawDashedRect(g, 0, y, getWidth(), h);
            g.setPaintMode();
          }

        } else {
          done = true;
        }
      }

      if (!done) {
        y += h;

        if (node.getFirstChild() != null && !node.isCollapsed()) {
          paintRecurse(g, node.getFirstChild(), y);
          y += node.getChildrenHeight();
        }
        node = node.getNextSibling();
      }
    }
  }

  /**
   * Recursive part of <code>hitTestNode()</code>
   * @see #hitTestNode
   */

  TreeNode hitTestNodeRecurse(TreeNode aFirst, int aTop, int aY) {
    TreeNode node = aFirst;
    int nodeHeight, totalHeight;

    while (node != null) {
      nodeHeight = node.getNodeHeight();
      totalHeight = node.getTotalHeight();
      if (aY < aTop + totalHeight) { // hit node or child
        if (aY < aTop + nodeHeight) { // hit node
          return node;
        } else { // hit child
          return hitTestNodeRecurse(node.getFirstChild(), aTop + nodeHeight, aY);
        }
      }
      node = node.getNextSibling();
      aTop += totalHeight;
    }
    return null;
  }

  /**
   * Determines what node is under the given coordinate
   */

  synchronized TreeNode hitTestNode(int aY) {
    return hitTestNodeRecurse(getVisibleRoot(), 0, aY);
  }

  /**
   * Determines what column is under the given coordinate
   */

  Column hitTestColumn(int aX) {
    int columnMargin = fColumnModel.getColumnMargin();
    int w, x = 0;
    fHitX = -1;

    Enumeration columns = fColumnModel.getColumns();
    while (columns.hasMoreElements()) {
      Column column = (Column) columns.nextElement();
      w = column.getWidth() + columnMargin;

      if (aX < x + w) {
        fHitX = aX - x;
        return column;
      }
      x += w;
    }

    return null;
  }

  //
  // Component Overloads
  //

  public synchronized void paintComponent(Graphics g) {
    fPaintedRows = 0;

    paintRecurse(g, getVisibleRoot(), 0);

//    System.out.println("Painted " + fPaintedRows + " Rows");

    if (fHeight < getHeight()) {
      Rectangle rect = new Rectangle(0, fHeight, getWidth(), getHeight() - fHeight).
                       intersection(g.getClipBounds());
      g.setColor(fWindowColor);
      g.fillRect(rect.x, rect.y, rect.width, rect.height);
    }
  }

  void repaint(TreeNode aNode) {
    int y = getNodeY(aNode);
    int h = aNode.getNodeHeight();
    repaint(0, y, getWidth(), h);
  }

  public void repaint(TreePath aPath) {
    int y = getNodeY(aPath);
    int h = getNodeHeight(aPath);
    repaint(0, y, getWidth(), h);
  }

  public void repaintFrom(TreePath aPath) {
    int y = getNodeY(aPath);
    repaint(0, y, getWidth(), getHeight() - y);
  }

  public Dimension getPreferredSize() {
    return new Dimension(fColumnModel.getTotalColumnWidth(), fHeight);
  }

  public Dimension getMaximumSize() {
    return getPreferredSize();
  }

  public Dimension getMinimumSize() {
    return getPreferredSize();
  }

  public void updateUI() {
    super.updateUI();

    fPlusIcon = new ImageIcon(getClass().getResource("images/plus.gif"));
    fMinusIcon = new ImageIcon(getClass().getResource("images/minus.gif"));

    fWindowColor = UIManager.getColor("window");
    fHighlightColor = UIManager.getColor("textHighlight");
  }
  public boolean isOpaque() {
    return true;
  }

  public boolean getAutoscrolls() {
    return true;
  }

  public Point getToolTipLocation(MouseEvent aEvent) {
    return fTooltipPoint;
  }

  public String getToolTipText(MouseEvent aEvent) {
    TreeNode node = hitTestNode(aEvent.getY());
    if (node != null) {
      Column column = hitTestColumn(aEvent.getX());

      if (column != null) {
        int indent = getIndent(column, node);
        CellRenderer renderer = column.getCellRenderer();
        Component component = renderer.getComponent();
        Object data = fDataModel.getData(node.getData(), column.getID());
        renderer.setValue(node.getData(), data, false);
        Dimension size = component.getSize();
        if (size.width + indent > column.getWidth()) {
          fTooltipPoint =
            new Point(getColumnX(column) + indent, getNodeY(node));
          return data.toString();
        }
      }
    }
    fTooltipPoint = null;
    return null;
  }

  //
  // Scrollable interface
  //

  public Dimension getPreferredScrollableViewportSize() {
    return getPreferredSize();
  }

  public int getScrollableBlockIncrement(Rectangle aVisible, int aOrient,
                                         int aDirection) {
    if (aOrient == SwingConstants.VERTICAL) {
      return aVisible.height;
    } else {
      return aVisible.width;
    }
  }

  public int getScrollableUnitIncrement(Rectangle aVisible, int aOrient,
                                        int aDirection) {
    if (aOrient == SwingConstants.VERTICAL) {
      if (fRowHeight != 0) {
        return fRowHeight + fRowMargin;
      }
    }
    return 10;
  }

  public boolean getScrollableTracksViewportHeight() {
    return false;
  }

  public boolean getScrollableTracksViewportWidth() {
    return false;
  }

  //
  // DropTargetComponentInterface
  //

  public void setDropTarget(DropTarget dt) throws IllegalArgumentException {
    fDropTarget = dt;
  }

  public DropTarget getGrendelDropTarget() {
    return fDropTarget;
  }

  //
  // TreeMouseListener class
  //

  class TreeMouseListener extends MouseAdapter implements MouseMotionListener {

    boolean isContextClick(MouseEvent aEvent) {
      return (aEvent.isPopupTrigger() ||
             (aEvent.getModifiers() & MouseEvent.BUTTON3_MASK) != 0);
    }

    MouseEvent convertMouseEvent(MouseEvent aEvent) {
      return SwingUtilities.convertMouseEvent(fTreeTable, aEvent,
                                       fEditor.getCellEditorComponent());
    }

    void dispatchMouseEvent(MouseEvent aEvent) {
     fEditor.getCellEditorComponent().dispatchEvent(convertMouseEvent(aEvent));
    }

    void checkEditor(MouseEvent aEvent, Column column, TreeNode node) {
      fEditor = column != null ? column.getCellEditor() : null;
      fEditing = false;

      if (fEditor != null) {
        TreePath path = buildTreePath(node);
        fEditor.setValue(node.getData(),
                         fDataModel.getData(node.getData(),column.getID()),
                         fSelection.isSelected(path));
        if (fEditor.isCellEditable(aEvent)) {
          fEditedColumn = column;
          fEditedNode = node;
          fEditor.addCellEditorListener(new TreeCellEditorListener());
          Component editorComponent = fEditor.getCellEditorComponent();
          Dimension preferredSize = editorComponent.getPreferredSize();
          add(editorComponent);
          int x = getColumnX(column);
          int w = column.getWidth();
          int indent = getIndent(column, node);
          x += indent;
          w -= indent;

          int y = getNodeY(path);
          int h = getNodeHeight(path);
          editorComponent.setLocation(x, y);
          editorComponent.setSize(w, h);

          fEditing = true;
          fEditor.startCellEditing(convertMouseEvent(aEvent));
        }
      }
    }

    public void mousePressed(MouseEvent aEvent) {
      fJustSelected = false;
      fMousinessAfoot = true;
      fHitPoint = new Point(aEvent.getPoint());
      fDragging = false;

      TreeNode node = hitTestNode(aEvent.getY());
      if (node != null) {
        Column column = hitTestColumn(aEvent.getX());

        if (column != null && !isContextClick(aEvent)) {
          if (column.getID().equals(fTreeColumn)) {
            int plusX = node.getVisibleDepth() * fIndentLevel;

            if (fHitX > plusX && fHitX < (plusX + fIndentLevel)) {
              if (node.isCollapsed()) {
                expand(node);
              } else {
                collapse(node);
              }
              requestFocus();
              fJustSelected = true; // Pretend we did a selection to
                                    // Avoid triggering editor
              return; // Don't do selection
            }
          }
        }

        if (fEditing) {
          if (fEditedColumn == column && fEditedNode == node) {
            dispatchMouseEvent(aEvent);
            return;
          } else {
            fEditor.cancelCellEditing();
            fEditing = false;
          }
        }

        checkEditor(aEvent, column, node);

        if (!fEditing) {
          requestFocus();
          setCaret(node);
          select(buildTreePath(fCaret), aEvent.getModifiers());
        }
      } else {
        requestFocus();
      }
    }

    public void mouseReleased(MouseEvent aEvent) {
      fDragging = false;
      if (fEditing) {
        dispatchMouseEvent(aEvent);
      } else if (isContextClick(aEvent)) {
        fSelection.contextClickSelection(aEvent);
      } else if (!fJustSelected) {
        TreeNode node = hitTestNode(aEvent.getY());
        if (node != null) {
          Column column = hitTestColumn(aEvent.getX());
          checkEditor(aEvent, column, node);
        }
      }
      fMousinessAfoot = false;
      fJustSelected = false;
      fHitPoint = null;
    }

    public void mouseClicked(MouseEvent aEvent) {
      if (fEditing) {
        dispatchMouseEvent(aEvent);
      } else if (aEvent.getClickCount() == 2) {
        fSelection.doubleClickSelection(aEvent);
      }
    }

    public void mouseMoved(MouseEvent aEvent) {
      if (fEditing) {
        dispatchMouseEvent(aEvent);
      }
    }

    public void mouseDragged(MouseEvent aEvent) {
      if (fEditing) {
        dispatchMouseEvent(aEvent);
      } else if (!fDragging && fHitPoint != null) {
        Rectangle box =
          DragSource.getDragSource(fTreeTable).getDragThresholdBBox(fTreeTable,
                                                                    fHitPoint);

        if (!box.contains(aEvent.getPoint())) {
          fSelection.dragSelection(aEvent);
        }
        fDragging = true;
      }
    }
  }

  //
  // TreeCellEditorListener class
  //

  class TreeCellEditorListener implements CellEditorListener {
    public void editingStarted(ChangeEvent aEvent) {
      fEditing = true;
    }

    public void editingStopped(ChangeEvent aEvent) {
      fEditing = false;
      fDataModel.setData(fEditedNode.getData(), fEditedColumn.getID(), fEditor.getCellEditorValue());
      Rectangle edRect = fEditor.getCellEditorComponent().getBounds();
      remove(fEditor.getCellEditorComponent());
      repaint(edRect);
      fEditor.removeCellEditorListener(this);
    }

    public void editingCanceled(ChangeEvent aEvent) {
      fEditing = false;
      Rectangle edRect = fEditor.getCellEditorComponent().getBounds();
      remove(fEditor.getCellEditorComponent());
      repaint(edRect);
      fEditor.removeCellEditorListener(this);
    }
  }

  //
  // TreeColumnModelListener Class
  //

  class TreeColumnModelListener implements ColumnModelListener {
    public void columnAdded(ColumnModelEvent e) {
      resizeAndRepaint();
    }

    public void columnRemoved(ColumnModelEvent e) {
      resizeAndRepaint();
    }

    public void columnMoved(ColumnModelEvent e) {
      repaint();
    }

    public void columnMarginChanged(ChangeEvent e) {
      resizeAndRepaint();
    }

    public void columnWidthChanged(ColumnModelEvent e) {
      resizeAndRepaint();
    }

    public void columnSelectionChanged(ChangeEvent e) {
    }
  }

  class TreeModelListener implements TreeTableModelListener {
    public void nodeExpanded(TreeTableModelEvent aEvent) {
      synchronized (fTreeTable) {
        // We take the leap of faith here that we're not
        // expanding an already expanded node
        TreeNode node = findTreeNode(aEvent.getPath());
        fHeight += node.expand();
      }
      resizeAndRepaintFrom(aEvent.getPath());
    }

    public void nodeCollapsed(TreeTableModelEvent aEvent) {
      synchronized (fTreeTable) {
        // We take the leap of faith here that we're not
        // collapsing an already collapsed node
        TreeNode node = findTreeNode(aEvent.getPath());

        if (fCaret != null && fCaret.isChildOf(node)) {
          if (fSelection.isSelected(buildTreePath(fCaret))) {
            select(buildTreePath(node), 0);
          }
          setCaret(node);
        }

        fHeight += node.collapse();
      }

      resizeAndRepaintFrom(aEvent.getPath());
    }

    public void nodeInserted(TreeTableModelEvent aEvent) {
      synchronized (fTreeTable) {
        TreePath path = aEvent.getPath();
        if (path.getPath().length == 0) { // something at the root changed
          reload();
          resizeAndRepaint();
        } else {
          TreeNode node = findTreeNode(path);
          if (node != null) { // visible node changed
            node.reload(); // Redo everything for now
            resizeAndRepaintFrom(aEvent.getPath());
          }
        }
      }
    }

    public void nodeDeleted(TreeTableModelEvent aEvent) {
      synchronized (fTreeTable) {
        TreePath path = aEvent.getPath();
        if (path.getPath().length == 0) { // something at the root changed
          reload();
          resizeAndRepaint();
        } else {
          TreeNode node = findTreeNode(path);
          if (node != null) { // visible node changed
            node.reload(); // Redo everything for now
            resizeAndRepaintFrom(aEvent.getPath());
          }
        }
      }
    }

    public void nodeChanged(TreeTableModelEvent aEvent) {
      repaint(aEvent.getPath());
    }
  }

  //
  // TreeSelectionListener class
  //

  class TreeSelectionListener implements SelectionListener {
    public void selectionChanged(SelectionEvent aEvent) {
//      repaint();
      int i;
      Object added[] = aEvent.getAdded();
      Object removed[] = aEvent.getRemoved();

      if (added != null) {
        if (fMousinessAfoot)
          fJustSelected = true;

        for (i = 0; i < added.length; i++) {
          repaint((TreePath) added[i]);
        }
      }
      if (removed != null) {
        for (i = 0; i < removed.length; i++) {
          repaint((TreePath) removed[i]);
        }
      }
    }

    public void selectionDoubleClicked(MouseEvent aEvent) {
    }
    public void selectionContextClicked(MouseEvent aEvent) {
    }
    public void selectionDragged(MouseEvent aEvent) {
    }
  }

  //
  // TreeFocusListener class
  //

  class TreeFocusListener implements FocusListener {
    public void focusGained(FocusEvent aEvent) {
      repaint();
    }
    public void focusLost(FocusEvent aEvent) {
      repaint();
    }
  }

  //
  // ActionListener Classes
  //

  class NavigateUpAction implements ActionListener {
    int fModifiers;

    NavigateUpAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateUp(fModifiers);
    }
  }

  class NavigateDownAction implements ActionListener {
    int fModifiers;

    NavigateDownAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateDown(fModifiers);
    }
  }

  class NavigateLeftAction implements ActionListener {
    int fModifiers;

    NavigateLeftAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateLeft(fModifiers);
    }
  }

  class NavigateRightAction implements ActionListener {
    int fModifiers;

    NavigateRightAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateRight(fModifiers);
    }
  }

  class NavigateHomeAction implements ActionListener {
    int fModifiers;

    NavigateHomeAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateHome(fModifiers);
    }
  }

  class NavigateEndAction implements ActionListener {
    int fModifiers;

    NavigateEndAction(int aModifiers) {
      fModifiers = aModifiers;
    }

    public void actionPerformed(ActionEvent aEvent) {
      navigateEnd(fModifiers);
    }
  }

  class OpenAction implements ActionListener {
    public void actionPerformed(ActionEvent aEvent) {
      fSelection.doubleClickSelection(null);
    }
  }

  //
  // TreeNode Class
  //

  class TreeNode {
    Object    fData;

    TreeNode  fParent;
    TreeNode  fFirstChild;
    TreeNode  fNextSibling;

    boolean   fBuilt = false;
    boolean   fFreeOnCollapse = true;
    boolean   fInvalid = true;
    boolean   fIsLeaf = false;
    int       fChildrenHeight = 0;
    int       fDepth = -1;
    int       fNodeHeight = 0;

    //
    // Constructor
    //

    public TreeNode(TreeTableDataModel aDataModel, TreeNode aParent, Object aData) {
      fDataModel = aDataModel;
      fData = aData;
      fParent = aParent;
      fIsLeaf = aDataModel.isLeaf(fData);

      if (!fIsLeaf && !isCollapsed()) {
        expand();
      }
    }

    //
    // Building
    //

    final void addChild(TreeNode aNode) {
      addChildAfter(getLastChild(), aNode);
    }

    final void addChildAfter(TreeNode aPrev, TreeNode aNode) {
      aNode.fParent = this;

      if (aPrev == null) { // add at beginning
        aNode.fNextSibling = fFirstChild;
        fFirstChild = aNode;
      } else {
        aPrev.addSiblingAfter(aNode);
      }
    }

    final void addSiblingAfter(TreeNode aNode) {
      aNode.fNextSibling = fNextSibling;
      fNextSibling = aNode;
    }

    //
    // Unbuilding
    //

    final int remove() {
      if (fParent != null) {
        fParent.removeChild(this);
      } else {
        TreeNode prev = getPrevSibling();
        prev.fNextSibling = fNextSibling;
      }
      return -getHeight();
    }

    final int removeChild(TreeNode aNode) {
      if (fFirstChild == aNode && aNode != null) {
        fFirstChild = aNode.fNextSibling;
      }
      aNode.fParent = null;
      return aNode.remove();
    }

    /**
     * Make garbage collection possible
     */

    final void detachChildren() {
      TreeNode node = fFirstChild;
      while (node != null) {
        TreeNode next = node.fNextSibling;
        node.detachSelf();
        node = next;
      }
      fFirstChild = null;
    }

    final void detachSelf() {
      detachChildren();
      fParent = null;
      fNextSibling = null;
    }

    void invalidateBranch() {
      TreeNode node = this;
      while (node != null) {
        fInvalid = true;
        node = node.getParent();
      }
    }

    public final int collapse() {
      int res = -getChildrenHeight();

      if (fFreeOnCollapse) {
        detachChildren();
        fBuilt = false;
      }
      invalidateBranch();

      return res;
    }

    public final int expand() {
      if (!fBuilt) {
        build();
      }

      invalidateBranch();

      return getChildrenHeight();
    }

    public final int reload() {
      fIsLeaf = fDataModel.isLeaf(fData);

      if (fIsLeaf || isCollapsed()) {
        return 0;
      }
      int oldChildHeight = getChildrenHeight();

      detachChildren();
      build();

      invalidateBranch();

      return getChildrenHeight() - oldChildHeight;
    }

    final void build() {
      if (!fIsLeaf) {
        Enumeration children = fDataModel.getChildren(fData);
        if (children == null) {
          Object node = fDataModel.getChild(fData);
          while (node != null) {
            addChild(new TreeNode(fDataModel, this, node));
            node = fDataModel.getNextSibling(node);
          }
        } else {
          while (children.hasMoreElements()) {
            addChild(new TreeNode(fDataModel, this, children.nextElement()));
          }
        }
      }
    }

    public final boolean isCollapsed() {
      return fDataModel.isCollapsed(buildTreePath(this));
    }

    public final boolean isLeaf() {
      return fIsLeaf;
    }

    public final int getVisibleDepth() {
      return fDataModel.showRoot() ? getDepth() : getDepth() - 1;
    }

    public final int getDepth() {
      if (fDepth < 0) {
        if (getParent() != null) {
          fDepth = getParent().getDepth() + 1;
        } else {
          fDepth = 0;
        }
      }
      return fDepth;
    }

    public final TreeNode getParent() {
      return fParent;
    }

    public final TreeNode getPrevSibling() {
      TreeNode node = (fParent != null) ? fParent.getFirstChild() : fRoot;
      while (node != null) {
        if (node.fNextSibling == this) {
          return node;
        }
        node = node.fNextSibling;
      }
      return null;
    }

    public final TreeNode getNextSibling() {
      return fNextSibling;
    }

    public final TreeNode getFirstChild() {
      return fFirstChild;
    }

    public final TreeNode getLastChild() {
      TreeNode node = fFirstChild;
      while (node != null && node.fNextSibling != null) {
        node = node.fNextSibling;
      }
      return node;
    }

    public final boolean isChildOf(TreeNode aNode) {
      TreeNode parent = fParent;
      while (parent != null) {
        if (parent == aNode) {
          return true;
        }
        parent = parent.fParent;
      }
      return false;
    }

    public final Object getData() {
      return fData;
    }

    public final int getNodeHeight() {
      if (fRowHeight != 0) {
        return fRowHeight;
      }
      if (fNodeHeight == 0) {
        Enumeration columns = fColumnModel.getColumns();
        while (columns.hasMoreElements()) {
          Column column = (Column) columns.nextElement();
          CellRenderer renderer = column.getCellRenderer();

          renderer.setValue(fData,
                            fDataModel.getData(fData, column.getID()),
                            false);

          Component component = renderer.getComponent();
          Dimension size = component.getPreferredSize();
          if (size.height > fNodeHeight) {
            fNodeHeight = size.height;
          }
        }
        Icon icon = fDataModel.getIcon(fData);
        if (icon != null && icon.getIconHeight() > fNodeHeight) {
          fNodeHeight = icon.getIconHeight();
        }
      }

      // If we're fixed, we can avoid this calculation
      if (fFixed) {
        fRowHeight = fNodeHeight;
      }

      return fNodeHeight;
    }

    public final int getChildrenHeight() {
      if (fInvalid) {
        fChildrenHeight = 0;
        TreeNode node = getFirstChild();
        while (node != null) {
          fChildrenHeight += node.getTotalHeight();
          node = node.getNextSibling();
        }
      }
      return fChildrenHeight;
    }

    public final int getTotalHeight() {
      return  getNodeHeight() + (isCollapsed() ? 0 : getChildrenHeight());
    }
  }

}
