/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.tools;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Stack;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

/**
 * A checkbox tree selection model.
 *
 * @author Doug Brown
 *
 * Based on code by Santhosh Kumar T - santhosh@in.fiorano.com
 * See http://www.jroller.com/page/santhosh/20050610
 */
public class CheckTreeSelectionModel extends DefaultTreeSelectionModel {
  private TreeModel model;
  private PropertyChangeSupport support;

  /**
   * Constructor.
   *
   * @param model a TreeModel
   */
  public CheckTreeSelectionModel(TreeModel model) {
    this.model = model;
    setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
    support = new SwingPropertyChangeSupport(this);
  }

  /**
   * Returns true if neither the path nor any descendant is selected.
   *
   * @param path the path to test
   */
  public boolean isPathUnselected(TreePath path) {
    if(isSelectionEmpty()) {
      return true; // nothing is selected
    }
    if(isPathOrAncestorSelected(path)) {
      return false; // path is selected
    }
    TreePath[] selectionPaths = getSelectionPaths();
    for(int i = 0; i<selectionPaths.length; i++) {
      if(path.isDescendant(selectionPaths[i])) { // found a selected descendant
        return false;
      }
    }
    return true;
  }

  /**
   * Returns true if the path or any ancestor is selected.
   *
   * @param path the path to test
   */
  public boolean isPathOrAncestorSelected(TreePath path) {
    while((path!=null)&&!isPathSelected(path)) {
      path = path.getParentPath();
    }
    return path!=null;
  }

  public void setSelectionPaths(TreePath[] paths) {
    super.clearSelection();
    addSelectionPaths(paths);
  }

  /**
   * Adds paths to the current selection
   *
   * @param paths the paths to add
   */
  public void addSelectionPaths(TreePath[] paths) {
    if(paths==null) {
      return;
    }
    TreePath[] prev = getSelectionPaths();
    // unselect all descendants of paths
    for(int i = 0; i<paths.length; i++) {
      if(isSelectionEmpty()) {
        break; // nothing to unselect
      }
      TreePath path = paths[i];
      TreePath[] selectionPaths = getSelectionPaths();
      ArrayList<TreePath> toBeRemoved = new ArrayList<TreePath>();
      for(int j = 0; j<selectionPaths.length; j++) {
        if(path.isDescendant(selectionPaths[j])) {
          toBeRemoved.add(selectionPaths[j]);
        }
      }
      super.removeSelectionPaths(toBeRemoved.toArray(new TreePath[0]));
    }
    // if all siblings selected, unselect them and select parent recursively, 
    // otherwise select the path
    for(int i = 0; i<paths.length; i++) {
      TreePath path = paths[i];
      TreePath temp = null;
      while(isSiblingsSelected(path)) {
        temp = path;
        if(path.getParentPath()==null) {
          break;
        }
        path = path.getParentPath();
      }
      if(temp!=null) {
        if(temp.getParentPath()!=null) {
          addSelectionPath(temp.getParentPath());
        } else { // selected path is root, so unselect all others
          if(!isSelectionEmpty()) {
            removeSelectionPaths(getSelectionPaths());
          }
          super.addSelectionPaths(new TreePath[] {temp});
        }
      } else {
        super.addSelectionPaths(new TreePath[] {path});
      }
    }
    support.firePropertyChange("treepaths", prev, getSelectionPaths()); //$NON-NLS-1$
  }

  /**
   * Removes paths from the current selection
   *
   * @param paths the paths to remove
   */
  public void removeSelectionPaths(TreePath[] paths) {
    if(isSelectionEmpty()) {
      return; // nothing to remove
    }
    TreePath[] prev = getSelectionPaths();
    for(int i = 0; i<paths.length; i++) {
      TreePath path = paths[i];
      if(path.getPathCount()==1) { // root node
        super.removeSelectionPaths(new TreePath[] {path});
      } else {
        if(isPathSelected(path)) {
          super.removeSelectionPaths(new TreePath[] {path});
        } else {
          unselectAncestor(path);
        }
      }
    }
    support.firePropertyChange("treepaths", prev, getSelectionPaths()); //$NON-NLS-1$
  }

  /**
   * Adds a PropertyChangeListener.
   *
   * @param listener the object requesting property change notification
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    support.addPropertyChangeListener(listener);
  }

  /**
   * Removes a PropertyChangeListener.
   *
   * @param listener the listener requesting removal
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    support.removePropertyChangeListener(listener);
  }

  /**
   * Determines whether all siblings of given path are selected
   *
   * @param path the path to test
   * @return true if all siblings selected
   */
  private boolean isSiblingsSelected(TreePath path) {
    TreePath parent = path.getParentPath();
    if(parent==null) {
      return true;
    }
    Object node = path.getLastPathComponent();
    Object parentNode = parent.getLastPathComponent();
    int childCount = model.getChildCount(parentNode);
    for(int i = 0; i<childCount; i++) {
      Object childNode = model.getChild(parentNode, i);
      if(childNode.equals(node)) {
        continue;
      }
      if(!isPathSelected(parent.pathByAddingChild(childNode))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Unselects the ancestor of a path and selects ancestor's descendants
   * other than those for which the path is a descendant.
   *
   * @param path the path
   */
  private void unselectAncestor(TreePath path) {
    // find selected ancestor 
    Stack<TreePath> stack = new Stack<TreePath>();
    stack.push(path);
    TreePath ancestor = path.getParentPath();
    while((ancestor!=null)&&!isPathSelected(ancestor)) {
      stack.push(ancestor);
      ancestor = ancestor.getParentPath();
    }
    if(ancestor==null) { // no selected ancestors!
      return;
    }
    stack.push(ancestor);
    // for each path in the stack, unselect it and select all child nodes
    while(!stack.isEmpty()) {
      TreePath next = stack.pop();
      super.removeSelectionPaths(new TreePath[] {next});
      if(stack.isEmpty()) {
        return;
      }
      Object node = next.getLastPathComponent();
      int childCount = model.getChildCount(node);
      for(int i = 0; i<childCount; i++) {
        Object child = model.getChild(node, i);
        super.addSelectionPaths(new TreePath[] {next.pathByAddingChild(child)});
      }
    }
  }

}

/*
 * Open Source Physics software is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be
 * released under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston MA 02111-1307 USA or view the license online at
 * http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007 The Open Source Physics project
 * http://www.opensourcephysics.org
 */
