package org.mbari.vdm.knowledgebase;


//~--- non-JDK imports --------------------------------------------------------

import java.awt.Cursor;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mbari.vars.dao.DAOException;
import org.mbari.vars.knowledgebase.model.Concept;
import org.mbari.vars.knowledgebase.model.dao.KnowledgeBaseCache;

/**
* A JTree implementation for displaying <code>KnowledgeBase</code>
* <code>Concept</code> objects in the <code>MaintGui</code>.
*
*
* @author  brian
* @created  November 11, 2004
* @version  $Id: ConceptTree.java,v 1.1 2005/11/16 17:36:54 achase Exp $
*/
public class ConceptTree extends JTree {

  //~--- static fields ------------------------------------------------------

  // RxTBD wcpr - Waiting for refactor to ConceptTree
  final static String ADD_CONCEPT = "Add Concept";
  final static String COLLAPSE_ALL = "Collapse All";
  final static String COLLAPSE_CHILDREN = "Collapse Children";
  
  public final static Cursor WAIT_CURSOR =
      Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
  
  public final static Cursor DEFAULT_CURSOR =
      Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);

  final static String EXPAND_ALL = "Expand All";

  final static String EXPAND_DESCENDENTS = "Expand Descendents";
  final static String REMOVE_CONCEPT = "Remove Concept";

  private static Log log = LogFactory.getLog(ConceptTree.class);


  //~--- constructors -------------------------------------------------------

  /**
   * Required for subclasses used by annotation ui
   *
   * @param  rootConcept
   */
  public ConceptTree(Concept rootConcept) {
      loadModel(rootConcept);
      initTreeProperties();
      setupListeners();
  }

  //~--- methods ------------------------------------------------------------

  /**
   * Adds a node to the specifed tree node for each of the children of the
   * specified <code>Concept.
   *
   * @param  node   The node to which to add children nodes.
   * @param  concept   The <code>Concept</code> whose children are to be added.
   */
  private void addChildrenNodes(DefaultMutableTreeNode node,
                                Concept concept) {
      Iterator iterator = concept.getChildConceptColl().iterator();

      while (iterator.hasNext()) {
          Concept childConcept = (Concept) iterator.next();
          DefaultMutableTreeNode childNode =
              new SortedTreeNode(new TreeConcept(childConcept));

          node.add(childNode);
          addChildrenNodes(childNode, childConcept);
      }
  }
  


  /**
   * Collapses all tree nodes.
   */
  public void collapseAll() {
      setCursor(WAIT_CURSOR);

      DefaultMutableTreeNode rootNode =
          (DefaultMutableTreeNode) getModel().getRoot();

      makeChildrenInvisible(rootNode);
      setCursor(DEFAULT_CURSOR);
  }

  /**
   * Collapses all children nodes under the currently selected node.
   */
  public void collapseChildren() {
      setCursor(WAIT_CURSOR);

      DefaultMutableTreeNode selectedNode =
          (DefaultMutableTreeNode) getSelectionPath().getLastPathComponent();

      makeChildrenInvisible(selectedNode);
      setCursor(DEFAULT_CURSOR);
  }

  /**
   * Expands all tree nodes.
   */
  public void expandAll() {
      setCursor(WAIT_CURSOR);

      DefaultMutableTreeNode rootNode =
          (DefaultMutableTreeNode) getModel().getRoot();

      makeChildrenVisible(rootNode);
      setCursor(DEFAULT_CURSOR);
  }

  /**
   * Expands all descendent nodes under the currently selected node.
   */
  public void expandDescendents() {
      setCursor(WAIT_CURSOR);

      DefaultMutableTreeNode selectedNode =
          (DefaultMutableTreeNode) getSelectionPath().getLastPathComponent();

      makeChildrenVisible(selectedNode);
      setCursor(DEFAULT_CURSOR);
  }

  /**
   * Expands all tree nodes from the root down to the specified node name. Does
   * not expand the final node as that node may not have any children.
   *
   * @param  name   The name of the final node.
   * @return    The final node, which itself has not been expanded.
   */
  DefaultMutableTreeNode expandDownToNode(String name) {
      /* 
       * Get a list of the family tree for the parameter concept. This list is
       * used to travel down the tree to the desired concept node.
       */
  	List list = null;
		try {
			list = KnowledgeBaseCache.getInstance().findConceptFamilyTree(name);
		} catch (DAOException e) {
			if (log.isErrorEnabled()) {
				log.error("Call to knowledgebase cache failed", e);
			}
			AppFrameDispatcher.showErrorDialog("There was a problem talking" +
					" to the database. Unable to open '" + name + 
					"' in the tree.");
		}
  	
      Iterator familyTree = list.iterator();

      // Pop the root Concept off the stack since it is the degenerative case.
      familyTree.next();

      // Then walk down the family tree, starting at the root node.
      DefaultMutableTreeNode treeNode =
          (DefaultMutableTreeNode) getModel().getRoot();

      while (familyTree.hasNext()) {
          String nextConceptName =
              ((Concept) familyTree.next()).getPrimaryConceptNameAsString();

          // Need to ensure the tree node for the current family name is expanded.
          TreeConcept treeConcept = (TreeConcept) treeNode.getUserObject();

          treeConcept.lazyExpand(treeNode);

          // Find the child node for the next family member.
          boolean found = false;
          Enumeration childrenNodes = treeNode.children();

          while (!found && childrenNodes.hasMoreElements()) {
              treeNode =
                  (DefaultMutableTreeNode) childrenNodes.nextElement();
              treeConcept = (TreeConcept) treeNode.getUserObject();

              if (nextConceptName.equals(treeConcept.getName())) {
                  found = true;
              }
          }
      }

      /* 
       * RxNOTE The final value of treeNode drops out of the above while loop
       * without a call to lazyExpand. This is purposeful as the final node may
       * or may not have children. We are only expanding down to the node, not
       * expanding the node itself.
       */

      return treeNode;
  }

  //~--- get methods --------------------------------------------------------

  /**
   * Gets the name of the <code>Concept</code> represented by the currently
   * selected node.
   *
   * @return    The name of the <code>Concept</code> represented by the currently
   *  selected node.
   */
  public String getSelectedName() {
      String name = null;
      DefaultMutableTreeNode node = getSelectedNode();

      if (node != null) {
          name = ((TreeConcept) node.getUserObject()).getName();
      }

      return name;
  }

  /**
   * Gets the currently selected tree node.
   *
   * @return    The currently selected tree node.
   */
  protected DefaultMutableTreeNode getSelectedNode() {
      DefaultMutableTreeNode node = null;
      TreePath path = getSelectionPath();

      if (path != null) {
          node = (DefaultMutableTreeNode) path.getLastPathComponent();
      }

      return node;
  }

  //~--- methods ------------------------------------------------------------

  /**
   * Initialize the properties of this <code>ConceptTree</code>.
   */
  protected void initTreeProperties() {
      putClientProperty("JTree.lineStyle", "Angled");
      getSelectionModel().setSelectionMode(
          TreeSelectionModel.SINGLE_TREE_SELECTION);

      // don't allow edits to the Concept names directly in this tree
      setEditable(false);
      setCellRenderer(new ConceptTreeCellRenderer());
      setRootVisible(true);
  }

  /**
   * Loads the JTree model using the specified <code>Concept</code> as the root
   * node.
   *
   * @param  rootConcept Description of the Parameter
   */
  public void loadModel(Concept rootConcept) {
      if (rootConcept == null) {
          setModel(null);
      } else {
          TreeConcept treeConcept = new TreeConcept(rootConcept);
          DefaultMutableTreeNode rootNode = new SortedTreeNode(treeConcept);

//        addChildrenNodes( rootNode, rootConcept );
          // adding a boolean value as a child to this concept indicates that
          // the node has children that will be loaded dynamically, start off by
          // assuming this node has children, the rest will be taken care of in
          // the ConceptTreeLazyLoader
          rootNode.add(new DefaultMutableTreeNode(Boolean.TRUE));
          treeConcept.lazyExpand(rootNode);

          DefaultTreeModel model = new DefaultTreeModel(rootNode);

          setModel(model);
          addTreeExpansionListener(new ConceptTreeLazyLoader(model));
      }
  }

  /**
   * Makes the children nodes under the specified node invisible.
   *
   * @param  node   The node on which to act.
   */
  private void makeChildrenInvisible(DefaultMutableTreeNode node) {
      Enumeration children = node.children();

      while (children.hasMoreElements()) {
          DefaultMutableTreeNode childNode =
              (DefaultMutableTreeNode) children.nextElement();

          if (!childNode.isLeaf()) {
              makeChildrenInvisible(childNode);

              TreeNode[] nodesFromRoot = childNode.getPath();
              TreePath pathFromRoot = new TreePath(nodesFromRoot);

              collapsePath(pathFromRoot);
          }
      }
  }

  /**
   * Makes the children nodes under the specified node visible.
   *
   * @param  node   The node on which to act.
   */
  private void makeChildrenVisible(DefaultMutableTreeNode node) {
      // RxTBD wcpr The Java API interaction of using TreeNodes and TreePaths
      // doesn't seem to make sense. There should be a cleaner way to implement
      // this method.

      if (node.isLeaf()) {
          return;
      }

      // Expand the node
      TreeConcept treeConcept = (TreeConcept) node.getUserObject();

      treeConcept.lazyExpand(node);

      boolean allChildrenAreLeaves = true;
      Enumeration children = node.children();

      while (children.hasMoreElements()) {
          DefaultMutableTreeNode childNode =
              (DefaultMutableTreeNode) children.nextElement();

          if (!childNode.isLeaf()) {
              makeChildrenVisible(childNode);

              allChildrenAreLeaves = false;
          }
      }

      if (allChildrenAreLeaves) {
          DefaultMutableTreeNode lastNode =
              (DefaultMutableTreeNode) node.getLastChild();
          TreeNode[] nodesFromRoot = node.getPath();
          TreePath pathFromRoot =
              new TreePath(nodesFromRoot).pathByAddingChild(lastNode);

          makeVisible(pathFromRoot);
      }
  }


  //~--- set methods --------------------------------------------------------

  /**
   * Sets the selected tree node to the node representing the specified
   * <code>Concept</code> name.
   *
   * @param  name The new selectedConcept value
   */
  public void setSelectedConcept(String name) {
      if (name == null) {
          return;
      }

      // RxNOTE Strategy: The tree node for the Concept being selected may not
      // yet be expanded, so expand down to the desired node.

      DefaultMutableTreeNode treeNode = expandDownToNode(name);

      // Now select the node and scroll to it.
      TreePath path = new TreePath(treeNode.getPath());

      setSelectionPath(path);
      scrollPathToVisible(path);
  }

  //~--- methods ------------------------------------------------------------

  /**
   * Sets up the various listeners needed for GUI interaction with this
   * <code>ConceptTree</code>.
   */
  protected void setupListeners() {
      // Add context popup menu and right mouse button selection
      addMouseListener(new MouseAdapter() {

          public void mousePressed(MouseEvent event) {
              // Selected item before showing popup menu
              if (event.getModifiers() == MouseEvent.BUTTON3_MASK) {
                  int row = getRowForLocation(event.getX(), event.getY());

                  setSelectionRow(row);
              }
          }

      });

      // Toggle expand/collapse on ENTER
      addKeyListener(new KeyAdapter() {

          public void keyReleased(KeyEvent event) {
              if (event.getKeyCode() == KeyEvent.VK_ENTER) {
                  int row = getSelectionRows()[0];

                  if (isCollapsed(row)) {
                      expandRow(row);
                  } else {
                      collapseRow(row);
                  }
              }
          }

      });
  }

  /**
   *  Description of the Method
   */
  void updateTreeNode() {
      DefaultMutableTreeNode selectedNode = getSelectedNode();
      selectedNode.setUserObject(new TreeConcept((Concept)Dispatcher.getDispatcher(Concept.class).getValueObject()));

      /* 
       * Announcing the node structure change triggers the necessary repaint to
       * display the effects of changes.
       */
      DefaultTreeModel model = (DefaultTreeModel) getModel();
      model.nodeStructureChanged(selectedNode);
  }

  /**
   * Sets the parent node of the currently selected node to be the node
   * representing the <code>Concept</code> of the specified name.
   *
   * @param  newParentName   The name of the <code>Concept</code> for which the currently selected
   *  node is to become a child.
   */
  public void updateTreeNodeParent(String newParentName) {
      // Get the node being moved
      DefaultMutableTreeNode conceptNode =
          (DefaultMutableTreeNode) getSelectionPath().getLastPathComponent();
      String conceptNodeName =
          ((TreeConcept) conceptNode.getUserObject()).getName();
      DefaultTreeModel treeModel = (DefaultTreeModel) getModel();

      // Remove node from current parent node and update structure
      DefaultMutableTreeNode parentNode =
          (DefaultMutableTreeNode) conceptNode.getParent();

      parentNode.remove(conceptNode);
      treeModel.nodeStructureChanged(parentNode);

      // Get the new parent node
      DefaultMutableTreeNode newParentNode = expandDownToNode(newParentName);
      TreeConcept treeConcept = (TreeConcept) newParentNode.getUserObject();
      boolean parentNeededExpanding = treeConcept.lazyExpand(newParentNode);

      // Branch on whether parent needed expanding:
      // - The parent node needed to be expanded. The call to lazyExpand()
      // updates the parent node's children so we don't need to explicitly add
      // the new child node. Find and select the new child node.
      // - The parent node is already expanded, so insert the new child node in
      // the appropriate slot and select the new child node.
      if (parentNeededExpanding) {
          Enumeration children = newParentNode.children();

          while (children.hasMoreElements()) {
              DefaultMutableTreeNode node =
                  (DefaultMutableTreeNode) children.nextElement();
              String nodeName =
                  ((TreeConcept) node.getUserObject()).getName();

              if (nodeName.equals(conceptNodeName)) {
                  setSelectionPath(new TreePath(node.getPath()));

                  break;
              }
          }
      } else {
          // Insert the node at the appropriate point in the new parent node.
          int insertPosition = 0;
          Enumeration children = newParentNode.children();

          while (children.hasMoreElements()) {
              DefaultMutableTreeNode node =
                  (DefaultMutableTreeNode) children.nextElement();
              String nodeName =
                  ((TreeConcept) node.getUserObject()).getName();

              if (0 < nodeName.compareTo(conceptNodeName)) {
                  break;
              } else {
                  insertPosition++;
              }
          }

          treeModel.insertNodeInto(conceptNode, newParentNode,
                                   insertPosition);
          setSelectionPath(new TreePath(conceptNode.getPath()));
      }
  }

}
