/*
 * $Id: TreeModelSupport.java 3100 2008-10-14 22:33:10Z rah003 $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.tree;

import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import org.jdesktop.swingx.util.Contract;

/**
 * Support for change notification, usable by {@code TreeModel}s.
 * 
 * The changed/inserted/removed is expressed in terms of a {@code TreePath},
 * it's up to the client model to build it as appropriate.
 * 
 * This is inspired by {@code AbstractTreeModel} from Christian Kaufhold,
 * www.chka.de.
 * 
 * TODO - implement and test precondition failure of added/removed notification
 * 
 * @author JW
 */
public final class TreeModelSupport {
    protected EventListenerList listeners;

    private TreeModel treeModel;

    /**
     * Creates the support class for the given {@code TreeModel}.
     * 
     * @param model the model to support
     * @throws NullPointerException if {@code model} is {@code null}
     */
    public TreeModelSupport(TreeModel model) {
        if (model == null)
            throw new NullPointerException("model must not be null");
        listeners = new EventListenerList();
        this.treeModel = model;
    }

//---------------------- structural changes on subtree
    
    /** 
     * Notifies registered TreeModelListeners that the tree's root has
     * been replaced. Can cope with a null root.
     */
    public void fireNewRoot() {

        Object root = treeModel.getRoot();

        /*
         * Undocumented. I think it is the only reasonable/possible solution to
         * use use null as path if there is no root. TreeModels without root
         * aren't important anyway, since JTree doesn't support them (yet).
         */
        TreePath path = (root != null) ? new TreePath(root) : null;
        fireTreeStructureChanged(path);
    }

    /**
     * Call when a node has changed its leaf state.<p>
     * 
     * PENDING: rename? Do we need it?
     * @param path the path to the node with changed leaf state.
     */
    public void firePathLeafStateChanged(TreePath path) {
        fireTreeStructureChanged(path);
    }

    /**
     * Notifies registered TreeModelListeners that the structure
     * below the node identified by the given path has been 
     * completely changed.
     * <p>
     * NOTE: the subtree path maybe null if the root is null. 
     * If not null, it must contain at least one element (the root).
     * 
     * @param subTreePath the path to the root of the subtree 
     *    whose structure was changed. 
     * @throws NullPointerException if the path is not null but empty
     *   or contains null elements.  
     */
    public void fireTreeStructureChanged(TreePath subTreePath) {
        if (subTreePath != null) {
            Contract.asNotNull(subTreePath.getPath(), 
                    "path must not contain null elements");
        }
        Object[] pairs = listeners.getListenerList();

        TreeModelEvent e = null;

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == TreeModelListener.class) {
                if (e == null)
                    e = createStructureChangedEvent(subTreePath);

                ((TreeModelListener) pairs[i + 1]).treeStructureChanged(e);
            }
        }
    }

//----------------------- node modifications, no mutations
    
    /**
     * Notifies registered TreeModelListeners that the 
     * the node identified by the given path has been modified.
     * 
     * @param path the path to the node that has been modified, 
     *   must not be null and must not contain null path elements.
     * 
     */
    public void firePathChanged(TreePath path) {
        Object node = path.getLastPathComponent();
        TreePath parentPath = path.getParentPath();

        if (parentPath == null)
            fireChildrenChanged(path, null, null);
        else {
            Object parent = parentPath.getLastPathComponent();

            fireChildChanged(parentPath, treeModel
                    .getIndexOfChild(parent, node), node);
        }
    }

    /**
     * Notifies registered TreeModelListeners that the given child of
     * the node identified by the given parent path has been modified.
     * The parent path must not be null, nor empty nor contain null
     * elements. 
     * 
     * @param parentPath the path to the parent of the modified children.
     * @param index the position of the child 
     * @param child child node that has been modified, must not be null
     */
    public void fireChildChanged(TreePath parentPath, int index, Object child) {
        fireChildrenChanged(parentPath, new int[] { index },
                new Object[] { child });
    }
    
    /**
     * Notifies registered TreeModelListeners that the given children of 
     * the node identified by the given parent path have been modified.
     * The parent path must not be null, nor empty nor contain null
     * elements. Note that the index array must contain the position of the
     * corresponding child in the the children array. The indices must be in
     * ascending order. <p>
     * 
     * The exception to these rules is if the root itself has been 
     * modified (which has no parent by definition). In this case  
     * the path must be the path to the root and both indices and children
     * arrays must be null.
     *  
     * @param parentPath the path to the parent of the modified children.
     * @param indices the positions of the modified children
     * @param children the modified children
     */
    public void fireChildrenChanged(TreePath parentPath, int[] indices,
            Object[] children) {
        Contract.asNotNull(parentPath.getPath(), 
                "path must not be null and must not contain null elements");
        Object[] pairs = listeners.getListenerList();

        TreeModelEvent e = null;

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == TreeModelListener.class) {
                if (e == null)
                    e = createTreeModelEvent(parentPath, indices, children);

                ((TreeModelListener) pairs[i + 1]).treeNodesChanged(e);
            }
        }
    }


//------------------------ mutations (insert/remove nodes)
    
    
    /**
     * Notifies registered TreeModelListeners that the child has been added to
     * the the node identified by the given parent path at the given position.
     * The parent path must not be null, nor empty nor contain null elements.
     * 
     * @param parentPath the path to the parent of added child.
     * @param index the position of the added children
     * @param child the added child
     */
    public void fireChildAdded(TreePath parentPath, int index, Object child) {
        fireChildrenAdded(parentPath, new int[] { index },
                new Object[] { child });
    }

    /**
     * Notifies registered TreeModelListeners that the child has been removed 
     * from the node identified by the given parent path from the given position.
     * The parent path must not be null, nor empty nor contain null elements.
     * 
     * @param parentPath the path to the parent of removed child.
     * @param index the position of the removed children before the removal
     * @param child the removed child
     */
    public void fireChildRemoved(TreePath parentPath, int index, Object child) {
        fireChildrenRemoved(parentPath, new int[] { index },
                new Object[] { child });
    }

    /**
     * Notifies registered TreeModelListeners that the given children have been
     * added to the the node identified by the given parent path at the given
     * locations. The parent path and the child array must not be null, nor
     * empty nor contain null elements. Note that the index array must contain
     * the position of the corresponding child in the the children array. The
     * indices must be in ascending order.
     * <p>
     * 
     * @param parentPath the path to the parent of the added children.
     * @param indices the positions of the added children.
     * @param children the added children.
     */
    public void fireChildrenAdded(TreePath parentPath, int[] indices,
            Object[] children) {
        Object[] pairs = listeners.getListenerList();

        TreeModelEvent e = null;

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == TreeModelListener.class) {
                if (e == null)
                    e = createTreeModelEvent(parentPath, indices, children);

                ((TreeModelListener) pairs[i + 1]).treeNodesInserted(e);
            }
        }
    }

    /**
     * Notifies registered TreeModelListeners that the given children have been
     * removed to the the node identified by the given parent path from the
     * given locations. The parent path and the child array must not be null,
     * nor empty nor contain null elements. Note that the index array must
     * contain the position of the corresponding child in the the children
     * array. The indices must be in ascending order.
     * <p>
     * 
     * @param parentPath the path to the parent of the removed children.
     * @param indices the positions of the removed children before the removal
     * @param children the removed children
     */
    public void fireChildrenRemoved(TreePath parentPath, int[] indices,
            Object[] children) {
        Object[] pairs = listeners.getListenerList();

        TreeModelEvent e = null;

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == TreeModelListener.class) {
                if (e == null)
                    e = createTreeModelEvent(parentPath, indices, children);
                ((TreeModelListener) pairs[i + 1]).treeNodesRemoved(e);
            }
        }
    }
    
//------------------- factory methods of TreeModelEvents
    
    /**
     * Creates and returns a TreeModelEvent for structureChanged 
     * event notification. The given path may be null to indicate
     * setting a null root. In all other cases, the first path element
     * must contain the root and the last path element the rootNode of the
     * structural change. Specifically, a TreePath with a single element
     * (which is the root) denotes a structural change of the complete tree.
     * 
     * @param parentPath the path to the root of the changed structure,
     *   may be null to indicate setting a null root.
     * @return a TreeModelEvent for structureChanged notification.
     * 
     * @see javax.swing.event.TreeModelEvent
     * @see javax.swing.event.TreeModelListener
     */
    private TreeModelEvent createStructureChangedEvent(TreePath parentPath) {
        return createTreeModelEvent(parentPath, null, null);
    }

    /**
     * Creates and returns a TreeModelEvent for changed/inserted/removed
     * event notification.
     * 
     * @param parentPath path to parent of modified node
     * @param indices the indices of the modified children (before the change)
     * @param children the array of modified children 
     * @return a TreeModelEvent for changed/inserted/removed notification
     * 
     * @see javax.swing.event.TreeModelEvent
     * @see javax.swing.event.TreeModelListener
     */
    private TreeModelEvent createTreeModelEvent(TreePath parentPath,
            int[] indices, Object[] children) {
        return new TreeModelEvent(treeModel, parentPath, indices, children);
    }


//------------------------ handling listeners
    
    public void addTreeModelListener(TreeModelListener l) {
        listeners.add(TreeModelListener.class, l);
    }

    public TreeModelListener[] getTreeModelListeners() {
        return listeners.getListeners(TreeModelListener.class);
    }

    public void removeTreeModelListener(TreeModelListener l) {
        listeners.remove(TreeModelListener.class, l);
    }
}
