/*
 * @(#)TreeSearchable.java
 *
 * Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
 */
package com.jidesoft.swing;

import javax.swing.*;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;

/**
 * <code>TreeSearchable</code> is an concrete implementation of {@link Searchable} that enables the search function in
 * JTree. <p>It's very simple to use it. Assuming you have a JTree, all you need to do is to call
 * <code><pre>
 * JTree tree = ....;
 * TreeSearchable searchable = new TreeSearchable(tree);
 * </pre></code>
 * Now the JTree will have the search function.
 * <p/>
 * There is very little customization you need to do to TreeSearchable. The only thing you might need is when the
 * element in the JTree needs a special conversion to convert to string. If so, you can override
 * convertElementToString() to provide you own algorithm to do the conversion.
 * <code><pre>
 * JTree tree = ....;
 * TreeSearchable searchable = new TreeSearchable(tree) {
 *      protected String convertElementToString(Object object) {
 *          ...
 *      }
 * };
 * </pre></code>
 * <p/>
 * Additional customization can be done on the base Searchable class such as background and foreground color,
 * keystrokes, case sensitivity. <p/> JTree actually has a simple searchable feature but has flaws. It will affect our
 * searchable feature. To workaround it, you can override getNextMatch method and always return -1 when you create your
 * JList. <code>
 * <pre>
 * JTree tree = new JTree(...) {
 *    public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
 *        return null;
 *    }
 * };
 * </pre>
 * </code>
 */
public class TreeSearchable extends Searchable implements TreeModelListener, PropertyChangeListener {

    private boolean _recursive = false;
    private transient List<TreePath> _treePathes;

    public TreeSearchable(JTree tree) {
        super(tree);
        if (tree.getModel() != null) {
            tree.getModel().addTreeModelListener(this);
        }

        tree.addPropertyChangeListener(JTree.TREE_MODEL_PROPERTY, this);
    }

    /**
     * Checks if the searchable is recursive.
     *
     * @return true if searchable is recursive.
     */
    public boolean isRecursive() {
        return _recursive;
    }

    /**
     * Sets the recursive attribute.
     * <p/>
     * If TreeSearchable is recursive, it will all tree nodes including those which are not visible to find the matching
     * node. Obviously, if your tree has unlimited number of tree nodes or a potential huge number of tree nodes (such
     * as a tree to represent file system), the recursive attribute should be false. To avoid this potential problem in
     * this case, we default it to false.
     *
     * @param recursive the attribute
     */
    public void setRecursive(boolean recursive) {
        _recursive = recursive;
        resetTreePathes();
    }

    @Override
    public void uninstallListeners() {
        super.uninstallListeners();
        if (_component instanceof JTree) {
            if (((JTree) _component).getModel() != null) {
                ((JTree) _component).getModel().removeTreeModelListener(this);
            }
        }
        _component.removePropertyChangeListener(JTree.TREE_MODEL_PROPERTY, this);
    }

    @Override
    protected void setSelectedIndex(int index, boolean incremental) {
        if (!isRecursive()) {
            if (incremental) {
                ((JTree) _component).addSelectionInterval(index, index);
            }
            else {
                ((JTree) _component).setSelectionRow(index);
            }
            ((JTree) _component).scrollRowToVisible(index);
        }
        else {
            Object elementAt = getElementAt(index);
            if (elementAt instanceof TreePath) { // else case should never happen
                TreePath path = (TreePath) elementAt;
                if (incremental) {
                    ((JTree) _component).addSelectionPath(path);
                }
                else {
                    ((JTree) _component).setSelectionPath(path);
                }
                ((JTree) _component).scrollPathToVisible(path);
            }
        }
    }

    @Override
    protected int getSelectedIndex() {
        if (!isRecursive()) {
            int ai[] = ((JTree) _component).getSelectionRows();
            return (ai != null && ai.length != 0) ? ai[0] : -1;
        }
        else {
            TreePath[] treePaths = ((JTree) _component).getSelectionPaths();
            if (treePaths != null && treePaths.length > 0) {
                return getTreePathes().indexOf(treePaths[0]);
            }
            else
                return -1;
        }
    }

    @Override
    protected Object getElementAt(int index) {
        if (index == -1) {
            return null;
        }
        if (!isRecursive()) {
            return ((JTree) _component).getPathForRow(index);
        }
        else {
            return getTreePathes().get(index);
        }
    }

    @Override
    protected int getElementCount() {
        if (!isRecursive()) {
            return ((JTree) _component).getRowCount();
        }
        else {
            return getTreePathes().size();
        }
    }

    /**
     * Recursively go through the tree to populate the tree paths into a list and cache them.
     * <p/>
     * Tree paths list is only used when recursive attribute is true.
     */
    protected void populateTreePaths() {
        _treePathes = new ArrayList<TreePath>();
        Object root = ((JTree) _component).getModel().getRoot();
        populateTreePaths0(root, new TreePath(root), ((JTree) _component).getModel());
    }

    private void populateTreePaths0(Object node, TreePath path, TreeModel model) {
        if (((JTree) _component).isRootVisible() || path.getLastPathComponent() != ((JTree) _component).getModel().getRoot()) {
            // if root not visible, do not add root
            _treePathes.add(path);
        }
        for (int i = 0; i < model.getChildCount(node); i++) {
            Object childNode = model.getChild(node, i);
            populateTreePaths0(childNode, path.pathByAddingChild(childNode), model);
        }
    }

    /**
     * Reset the cached tree paths list.
     * <p/>
     * Tree paths list is only used when recursive atattributes true.
     */
    protected void resetTreePathes() {
        _treePathes = null;
    }

    /**
     * Gets the cached tree paths list. If it has never been cached before, this method will create the cache.
     * <p/>
     * Tree paths list is only used when recursive atattributes true.
     *
     * @return the tree paths list.
     */
    protected List<TreePath> getTreePathes() {
        if (_treePathes == null) {
            populateTreePaths();
        }
        return _treePathes;
    }

    /**
     * Converts the element in JTree to string. The element by default is TreePath. The returned value will be
     * <code>toString()</code> of the last path component in the TreePath.
     *
     * @param object the object to be converted
     * @return the string representing the TreePath in the JTree.
     */
    @Override
    protected String convertElementToString(Object object) {
        if (object instanceof TreePath) {
            Object treeNode = ((TreePath) object).getLastPathComponent();
            if (getComponent() instanceof JTree) {
                JTree tree = (JTree) getComponent();
                TreePath[] selectionPaths = tree.getSelectionPaths();
                boolean selected = false;
                if (selectionPaths != null) {
                    for (TreePath selectedPath : selectionPaths) {
                        if (selectedPath == object) {
                            selected = true;
                            break;
                        }
                    }
                }
                return tree.convertValueToText(treeNode, selected, tree.isExpanded((TreePath) object), tree.getModel().isLeaf(treeNode), tree.getRowForPath((TreePath) object), tree.hasFocus() && tree.getLeadSelectionPath() == object);
            }
            return treeNode.toString();
        }
        else if (object != null) {
            return object.toString();
        }
        else {
            return "";
        }
    }

    public void treeNodesChanged(TreeModelEvent e) {
        if (!isProcessModelChangeEvent()) {
            return;
        }
        hidePopup();
        resetTreePathes();
    }

    public void treeNodesInserted(TreeModelEvent e) {
        if (!isProcessModelChangeEvent()) {
            return;
        }
        hidePopup();
        resetTreePathes();
    }

    public void treeNodesRemoved(TreeModelEvent e) {
        if (!isProcessModelChangeEvent()) {
            return;
        }
        hidePopup();
        resetTreePathes();
    }

    public void treeStructureChanged(TreeModelEvent e) {
        if (!isProcessModelChangeEvent()) {
            return;
        }
        hidePopup();
        resetTreePathes();
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if (JTree.TREE_MODEL_PROPERTY.equals(evt.getPropertyName())) {
            hidePopup();

            if (evt.getOldValue() instanceof TreeModel) {
                ((TreeModel) evt.getOldValue()).removeTreeModelListener(this);
            }

            if (evt.getNewValue() instanceof TreeModel) {
                ((TreeModel) evt.getNewValue()).addTreeModelListener(this);
            }

            resetTreePathes();
        }
    }
}
