/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.ui;

import org.broad.igv.logging.*;
import org.broad.igv.track.AttributeManager;
import org.broad.igv.ui.color.ColorUtilities;
import org.broad.igv.ui.util.LinkCheckBox;
import org.broad.igv.util.ResourceLocator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.List;

import static org.broad.igv.util.ResourceLocator.AttributeType.*;

/**
 * Parses XML file of IGV resources, and displays them in tree format.
 *
 * @author eflakes
 */
public class ResourceTree {

    private StringBuffer buffer = new StringBuffer();
    private static String FAILED_TO_CREATE_RESOURCE_TREE_DIALOG = "Failure while creating the resource tree dialog";
    private static Logger log = LogManager.getLogger(ResourceTree.class);
    private static String XML_ROOT = "Global";
    private List<CheckableResource> leafResources = new ArrayList();
    private HashMap<String, TreeNode> leafNodeMap = new HashMap();
    private JTree tree;
    private Set<String> selectedLeafNodePaths = new LinkedHashSet();

    private static enum TreeExpansionFlag {
        EXPAND_ALL,
        EXPAND_ROOT_ONLY,
        EXPAND_SELECTED_ONLY,
    }

    private ResourceTree() {
    }

    static class CancelableOptionPane extends JOptionPane {
        private boolean canceled = false;

        CancelableOptionPane(Object o, int i, int i1) {
            super(o, i, i1);
        }

        public boolean isCanceled() {
            return canceled;
        }

        public void setCanceled(boolean canceled) {
            this.canceled = canceled;
        }
    }


    /**
     * Shows a tree of selectable resources.
     *
     * @param document    The document that represents an XML resource tree.
     * @param dialogTitle
     * @return the resources selected by user.
     */
    public static LinkedHashSet<ResourceLocator> showResourceTreeDialog(Component parent, Document document, String dialogTitle) {

        JDialog dialog = null;

        final LinkedHashSet<ResourceLocator> locators = new LinkedHashSet();
        try {

            final ResourceTree resourceTree = new ResourceTree();
            final JTree dialogTree = resourceTree.createTreeFromDOM(document);

            int optionType = JOptionPane.OK_CANCEL_OPTION;
            int messageType = JOptionPane.PLAIN_MESSAGE;
            final CancelableOptionPane optionPane =
                    new CancelableOptionPane(new JScrollPane(dialogTree), messageType, optionType);

            optionPane.setPreferredSize(new Dimension(650, 500));
            optionPane.setOpaque(true);
            optionPane.setBackground(Color.WHITE);
            optionPane.addPropertyChangeListener(JOptionPane.VALUE_PROPERTY,
                    new PropertyChangeListener() {

                        public void propertyChange(PropertyChangeEvent e) {

                            Object value = e.getNewValue();
                            if (value instanceof Integer) {
                                int option = (Integer) value;
                                if (option == JOptionPane.CANCEL_OPTION) {
                                    optionPane.setCanceled(true);

                                } else {

                                    LinkedHashSet<ResourceLocator> selectedLocators =
                                            resourceTree.getSelectedResourceLocators();
                                    for (ResourceLocator locator : selectedLocators) {
                                        locators.add(locator);
                                    }
                                }
                            }
                        }
                    });

            dialog = optionPane.createDialog(parent, (dialogTitle == null ? "Resource Tree" : dialogTitle));
            dialog.setBackground(Color.WHITE);
            dialog.getContentPane().setBackground(Color.WHITE);

            Component[] children = optionPane.getComponents();
            if (children != null) {
                for (Component child : children) {
                    child.setBackground(Color.WHITE);
                }
            }

            dialog.setResizable(true);
            dialog.pack();
            dialog.setLocationRelativeTo(parent);
            dialog.setVisible(true);

            return optionPane.isCanceled() ? null : locators;
        } catch (Exception e) {
            log.error(FAILED_TO_CREATE_RESOURCE_TREE_DIALOG, e);
            return null;
        }
    }

    private void initTree(DefaultMutableTreeNode rootNode) {
        tree = new JTree(rootNode);
        tree.setExpandsSelectedPaths(true);
        tree.setCellRenderer(new NodeRenderer());
        tree.setCellEditor(new ResourceEditor(tree));
        tree.setEditable(true);
    }

    private JTree createTreeFromDOM(Document document) {

        Element rootElement =
                (Element) document.getElementsByTagName(XML_ROOT).item(0);

        if (rootElement == null) {
            return new JTree(new DefaultMutableTreeNode(""));
        }

        String nodeName = rootElement.getNodeName();
        if (!nodeName.equalsIgnoreCase(XML_ROOT)) {
            throw new RuntimeException(rootElement +
                    " is not the root of the xml document!");
        }

        String rootLabel = getAttribute(rootElement, "name");
        DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(rootLabel);

        initTree(rootNode);

        Set<ResourceLocator> loadedResources = IGV.hasInstance() ?
                IGV.getInstance().getDataResourceLocators() : Collections.<ResourceLocator>emptySet();
        loadedResources.addAll(AttributeManager.getInstance().getLoadedResources());

        // Build and attach descendants of the root node to the tree
        buildLocatorTree(rootNode, rootElement, loadedResources, this);

        // Force proper checks on startup
        recheckTree();

        expandTree(TreeExpansionFlag.EXPAND_SELECTED_ONLY, true);
        tree.updateUI(); // Tree state not always correct without this call
        return tree;
    }

    /**
     * Build a tree of all resources, placed under {@code treeNode}, starting
     * from {@code xmlNode}.
     *
     * @param treeNode
     * @param xmlNode
     * @param alreadyLoaded Resources which have already been loaded. These will be checked
     * @param resourceTree  ResourceTree instance. Can be null if not intending UI interaction
     */
    public static void buildLocatorTree(DefaultMutableTreeNode treeNode, Element xmlNode,
                                        Set<ResourceLocator> alreadyLoaded, ResourceTree resourceTree) {

        String name = getAttribute(xmlNode, NAME.getText());

        ResourceLocator locator = new ResourceLocator(
                getAttribute(xmlNode, PATH.getText())
        );
        locator.setName(name);

        String infoLink = getAttribute(xmlNode, HYPERLINK.getText());
        if (infoLink == null) {
            infoLink = getAttribute(xmlNode, INFOLINK.getText());
        }
        locator.setTrackInfoURL(infoLink);

        if (xmlNode.getTagName().equalsIgnoreCase("Resource")) {

            String resourceType = getAttribute(xmlNode, RESOURCE_TYPE.getText());
            locator.setFormat(resourceType);

            String sampleId = getAttribute(xmlNode, SAMPLE_ID.getText());
            if (sampleId == null) {
                // legacy option
                sampleId = getAttribute(xmlNode, ID.getText());
            }
            locator.setIndexPath(getAttribute(xmlNode, INDEX.getText()));
            locator.setSampleId(sampleId);
            locator.setFeatureInfoURL(getAttribute(xmlNode, URL.getText()));
            locator.setLabelField(getAttribute(xmlNode, LABEL_FIELD.getText()));
            locator.setDescription(getAttribute(xmlNode, DESCRIPTION.getText()));
            locator.setTrackLine(getAttribute(xmlNode, TRACK_LINE.getText()));
            locator.setName(name);
            // Special element for alignment tracks
            locator.setCoverage(getAttribute(xmlNode, COVERAGE.getText()));
            locator.setMappingPath(getAttribute(xmlNode, MAPPING.getText()));

            String colorString = getAttribute(xmlNode, COLOR.getText());
            if (colorString != null) {
                try {
                    Color c = ColorUtilities.stringToColor(colorString);
                    locator.setColor(c);
                } catch (Exception e) {
                    log.error("Error setting color: ", e);
                }
            }
        }

        NodeList nodeList = xmlNode.getChildNodes();
        Node xmlChildNode;

        // If we have children treat it as a category not a leaf
        for (int i = 0; i < nodeList.getLength(); i++) {

            xmlChildNode = nodeList.item(i);
            String nodeName = xmlChildNode.getNodeName();
            if (nodeName.equalsIgnoreCase("#text")) {
                continue;
            }

            // Need to check class of child node, its not necessarily an
            // element (could be a comment for example).
            if (xmlChildNode instanceof Element) {
                String categoryLabel = getAttribute((Element) xmlChildNode, NAME.getText());
                DefaultMutableTreeNode treeChildNode = new DefaultMutableTreeNode(categoryLabel);
                treeNode.add(treeChildNode);
                buildLocatorTree(treeChildNode, (Element) xmlChildNode, alreadyLoaded, resourceTree);
            }
        }

        CheckableResource resource = new CheckableResource(name, false, locator);
        treeNode.setUserObject(resource);

        if (resourceTree != null) {
            resource.setEnabled(resourceTree.tree.isEnabled());

            // If it's a leaf set the checkbox to represent the resource
            if (treeNode.isLeaf()) {

                resourceTree.expandPath(new TreePath(treeNode.getPath()));
                treeNode.setAllowsChildren(false);

                // If data already loaded disable the check box
                if (alreadyLoaded.contains(locator)) {
                    resource.setEnabled(false);
                    resource.setSelected(true);
                    resourceTree.checkParentNode(treeNode, true);
                }
                resourceTree.leafResources.add(resource);
            } else {

                treeNode.setAllowsChildren(true);

                boolean hasSelectedChildren = resourceTree.hasSelectedChildren(treeNode);
                resource.setSelected(hasSelectedChildren);

                if (true || hasSelectedChildren) {
                    ResourceEditor.checkOrUncheckParentNodesRecursively(treeNode, true);
                }
            }

            // Store the paths to all the leaf nodes for easy access
            if (treeNode.isLeaf()) {
                resourceTree.leafNodeMap.put(resourceTree.getPath(treeNode), treeNode);
            }
        }
    }

    public TreeNode checkParentNode(TreeNode childNode, boolean isSelected) {

        TreeNode parentNode = childNode.getParent();
        Object parentsUserObject =
                ((DefaultMutableTreeNode) parentNode).getUserObject();

        if (parentsUserObject instanceof CheckableResource) {
            CheckableResource parentResource =
                    ((CheckableResource) parentsUserObject);

            if (parentResource.isEnabled()) {
                parentResource.setSelected(isSelected);
            }
        }

        return parentNode;
    }

    public List<CheckableResource> getLeafResources() {
        return leafResources;
    }

    public LinkedHashSet<ResourceLocator> getSelectedResourceLocators() {

        LinkedHashSet<ResourceLocator> resourceLocators = new LinkedHashSet();

        for (CheckableResource resource : leafResources) {

            if (resource.isSelected()) {
                resourceLocators.add(resource.getResourceLocator());
            }
        }
        return resourceLocators;
    }

    private static String getAttribute(Element element, String key) {

        String value = element.getAttribute(key);
        if (value != null) {
            if (value.trim().equals("")) {
                value = null;
            }
        }
        return value;
    }


    private LinkedHashSet<DefaultMutableTreeNode> findSelectedLeafNodes(
            TreeModel model, TreeNode parentNode, LinkedHashSet<DefaultMutableTreeNode> allCheckedLeafNodes) {

        if (allCheckedLeafNodes == null) {
            allCheckedLeafNodes = new LinkedHashSet();
        }

        int count = model.getChildCount(parentNode);
        for (int i = 0; i < count; i++) {
            TreeNode childNode = (TreeNode) model.getChild(parentNode, i);
            if (!childNode.isLeaf()) {
                findSelectedLeafNodes(model, childNode, allCheckedLeafNodes);
            } else {
                DefaultMutableTreeNode node = (DefaultMutableTreeNode) childNode;
                CheckableResource resource = (CheckableResource) node.getUserObject();

                if (resource.isSelected()) {
                    allCheckedLeafNodes.add(node);
                }
            }
        }

        return allCheckedLeafNodes;
    }

    /**
     * Node's Renderer
     */
    static class NodeRenderer implements TreeCellRenderer {

        private LinkCheckBox renderer = new LinkCheckBox();
        private Color selectionForeground;
        private Color selectionBackground;
        private Color textForeground;
        private Color textBackground;

        public NodeRenderer() {

            Font fontValue;
            fontValue = UIManager.getFont("Tree.font");
            if (fontValue != null) {
                renderer.setFont(fontValue);
            }
            Boolean booleanValue =
                    (Boolean) UIManager.get("Tree.drawsFocusBorderAroundIcon");
            renderer.setFocusPainted(
                    (booleanValue != null) && (booleanValue.booleanValue()));

            selectionForeground = UIManager.getColor("Tree.selectionForeground");
            selectionBackground = UIManager.getColor("Tree.selectionBackground");
            textForeground = UIManager.getColor("Tree.textForeground");
            textBackground = UIManager.getColor("Tree.textBackground");
            renderer.setSelected(false);
        }

        public Component getTreeCellRendererComponent(JTree tree, Object value,
                                                      boolean isNodeSelected, boolean isNodeExpanded, boolean isLeaf,
                                                      int row, boolean hasFocus) {

            // Convert value into a usable string
            String stringValue = "";
            if (value != null) {
                String toStringValue = value.toString();
                if (toStringValue != null) {
                    stringValue = toStringValue;
                }
            }

            // Initialize checkbox state and selection
            renderer.setSelected(false);
            renderer.setText(stringValue);
            renderer.setEnabled(tree.isEnabled());

            // Tell renderer how to highlight nodes on selection
            if (isNodeSelected) {
                renderer.setForeground(selectionForeground);
                renderer.setBackground(selectionBackground);
            } else {
                renderer.setForeground(textForeground);
                renderer.setBackground(textBackground);
            }

            if (value != null) {
                if (value instanceof DefaultMutableTreeNode) {

                    DefaultMutableTreeNode node =
                            (DefaultMutableTreeNode) value;

                    Object userObject = node.getUserObject();
                    if (userObject instanceof CheckableResource) {

                        CheckableResource resource = (CheckableResource) userObject;
                        renderer.setText(resource.getText());
                        renderer.setSelected(resource.isSelected());
                        renderer.setEnabled(resource.isEnabled());

                        String hyperLink = resource.getResourceLocator().getTrackInfoURL();
                        if (hyperLink == null) {
                            renderer.showHyperLink(false);
                        } else {
                            renderer.setHyperLink(hyperLink);
                            renderer.showHyperLink(true);
                        }
                    }
                }
            }

            return renderer;
        }

        protected LinkCheckBox getRendereringComponent() {
            return renderer;
        }
    }

    /**
     * Node's Resource Editor
     */
    static class ResourceEditor extends AbstractCellEditor
            implements TreeCellEditor {

        NodeRenderer renderer = new NodeRenderer();
        JTree tree;

        public ResourceEditor(JTree tree) {
            this.tree = tree;
        }

        public Object getCellEditorValue() {

            DataResource resource = null;
            TreePath treePath = tree.getEditingPath();
            if (treePath != null) {

                Object node = treePath.getLastPathComponent();

                if ((node != null) && (node instanceof DefaultMutableTreeNode)) {

                    LinkCheckBox checkbox = renderer.getRendereringComponent();

                    DefaultMutableTreeNode treeNode =
                            (DefaultMutableTreeNode) node;

                    Object userObject = treeNode.getUserObject();

                    resource = (CheckableResource) userObject;

                    // Don't change resource if disabled
                    if (!resource.isEnabled()) {
                        return resource;
                    }

                    boolean isChecked = checkbox.isSelected();

                    // Check/Uncheck the selected node. This code ONLY handles
                    // the clicked node. Not it's ancestors or decendants.
                    if (isChecked) {
                        ((CheckableResource) resource).setSelected(true);
                    } else {

                        // See if we are allowed to unchecking this specific 
                        // node - if not, it won't be done. This does not 
                        // prevent it's children from being unchecked.
                        uncheckCurrentNodeIfAllowed((CheckableResource) resource,
                                treeNode);
                    }


                    /*
                    * Now we have to check or uncheck the descendants and
                    * ancestors depending on what we did above.
                    */

                    boolean checkRelatives = isChecked;

                    // If we found a mix of select leave and selected but
                    // but disabled leave we must be trying to toggle off
                    // the children
                    if (hasSelectedAndLockedDescendants(treeNode)) {
                        checkRelatives = false;
                    }
                    // If we found only locked leave we must be trying to toggle 
                    // on the unlocked children
                    else if (hasLockedDescendants(treeNode)) {
                        checkRelatives = true;
                    }
                    // Otherwise, just use the value of the checkbox


                    if (!treeNode.isLeaf()) { //check up and down the tree

                        // If not a leaf check/uncheck children as requested
                        checkOrUncheckChildNodesRecursively(treeNode, checkRelatives);

                        // If not a leaf check/uncheck ancestors
                        checkOrUncheckParentNodesRecursively(treeNode,
                                ((CheckableResource) resource).isSelected());
                    } else { // it must be a leaf - so check up the tree
                        checkOrUncheckParentNodesRecursively(treeNode, checkRelatives);
                    }
                }
                tree.treeDidChange();
            }
            return resource;
        }

        /*
        * Uncheck a node unless rule prevent this behavior.
        */

        private void uncheckCurrentNodeIfAllowed(CheckableResource resource,
                                                 TreeNode treeNode) {

            // If we are unchecking a parent make sure there are
            // no checked children
            if (!hasSelectedChildren(treeNode)) {
                ((CheckableResource) resource).setSelected(false);
            } else {

                // If node has selected children and has disabled descendants we
                // must not unselect
                if (hasLockedDescendants(treeNode)) {
                    ((CheckableResource) resource).setSelected(true);
                } else {
                    // No disabled descendants so we can uncheck at will
                    ((CheckableResource) resource).setSelected(false);
                }
            }
        }

        /**
         * Call to recursively check or uncheck the parent ancestors of the
         * passed node.
         */
        static public void checkOrUncheckParentNodesRecursively(TreeNode node,
                                                                boolean checkParentNode) {

            if (node == null) {
                return;
            }

            TreeNode parentNode = node.getParent();
            if (parentNode == null) {
                return;
            }

            Object parentUserObject =
                    ((DefaultMutableTreeNode) parentNode).getUserObject();

            CheckableResource parentNodeResource = null;
            if (parentUserObject instanceof CheckableResource) {
                parentNodeResource = ((CheckableResource) parentUserObject);
            }

            if (parentNodeResource != null) {

                // If parent's current check state matchs what we want there
                // is nothing to do so just leave
                if (parentNodeResource.isSelected() == checkParentNode) {
                    return;
                } else if (checkParentNode) {
                    parentNodeResource.setSelected(true);
                } else { // Uncheck Only if their are no selected descendants

                    if (!hasSelectedChildren(parentNode)) {
                        parentNodeResource.setSelected(false);
                    }
                }
            }

            checkOrUncheckParentNodesRecursively(parentNode,
                    checkParentNode);
        }

        /**
         * Can only be called from getCellEditorValue() to recursively check
         * or uncheck the children of the passed parent node.
         */
        private void checkOrUncheckChildNodesRecursively(TreeNode currentNode,
                                                         boolean isCheckingNeeded) {

            Object parentUserObject =
                    ((DefaultMutableTreeNode) currentNode).getUserObject();

            CheckableResource currentTreeNodeResource = null;
            if (parentUserObject instanceof CheckableResource) {
                currentTreeNodeResource = ((CheckableResource) parentUserObject);
            }

            if (currentTreeNodeResource != null) {

                // Set all enabled children to the checked state of their parent
                Enumeration children = currentNode.children();
                while (children.hasMoreElements()) {

                    TreeNode childNode = (TreeNode) children.nextElement();

                    Object childsUserObject =
                            ((DefaultMutableTreeNode) childNode).getUserObject();
                    if (childsUserObject instanceof CheckableResource) {

                        CheckableResource childResource =
                                ((CheckableResource) childsUserObject);

                        if (childResource.isEnabled()) {

                            // Child must be checked if it has selected
                            // selected and disabled descendants
                            if (hasLockedDescendants(childNode)) {
                                childResource.setSelected(true);
                            } else { // else check/uncheck  as requested
                                childResource.setSelected(isCheckingNeeded);
                            }
                        }
                    }
                    checkOrUncheckChildNodesRecursively(childNode,
                            isCheckingNeeded);
                }
            }
        }

        public boolean hasLockedDescendants(TreeNode treeNode) {

            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {
                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);

                    // If disabled say so
                    if (!childResource.isEnabled()) {
                        return true;
                    }
                }

                // If a descendant is disabled say so
                if (hasLockedDescendants(childNode)) {
                    return true;
                }
            }
            return false;
        }

        static public boolean hasSelectedDescendants(TreeNode treeNode) {

            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {
                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);

                    // If has selected say so
                    if (childResource.isSelected()) {
                        return true;
                    }
                }

                // If has selected descendant say so
                if (hasSelectedDescendants(childNode)) {
                    return true;
                }
            }
            return false;
        }

        static public boolean hasSelectedChildren(TreeNode treeNode) {

            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {
                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);
                    if (childResource.isSelected()) {
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * Return true if it find nodes that ar both selected and disabled
         *
         * @param treeNode
         * @return true if we are working with preselected nodes
         */
        public boolean hasLockedChildren(TreeNode treeNode) {

            boolean hasSelectedAndDisabled = false;
            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {

                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);

                    if (!childResource.isEnabled() && childResource.isSelected()) {
                        hasSelectedAndDisabled = true;
                    }

                    if (hasSelectedAndDisabled) {
                        break;
                    }
                }
            }
            return (hasSelectedAndDisabled);
        }

        /**
         * @param treeNode
         * @return true if we are working with preselected nodes
         */
        public boolean hasSelectedAndLockedChildren(TreeNode treeNode) {

            boolean hasSelected = false;
            boolean hasSelectedAndDisabled = false;
            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {

                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);

                    if (childResource.isSelected() && childResource.isEnabled()) {
                        hasSelected = true;
                    }
                    if (!childResource.isEnabled() && childResource.isSelected()) {
                        hasSelectedAndDisabled = true;
                    }

                    if (hasSelected & hasSelectedAndDisabled) {
                        break;
                    }
                }
            }

            // If we have both we can return true
            return (hasSelected & hasSelectedAndDisabled);
        }

        /**
         * @param treeNode
         * @return true if we are working with preselected nodes
         */
        public boolean hasSelectedAndLockedDescendants(TreeNode treeNode) {

            boolean hasSelected = false;
            boolean hasSelectedAndDisabled = false;
            Enumeration children = treeNode.children();
            while (children.hasMoreElements()) {

                TreeNode childNode = (TreeNode) children.nextElement();

                Object childsUserObject =
                        ((DefaultMutableTreeNode) childNode).getUserObject();
                if (childsUserObject instanceof CheckableResource) {

                    CheckableResource childResource =
                            ((CheckableResource) childsUserObject);

                    if (childResource.isSelected() && childResource.isEnabled()) {
                        hasSelected = true;
                    }
                    if (!childResource.isEnabled() && childResource.isSelected()) {
                        hasSelectedAndDisabled = true;
                    }

                    if (hasSelected & hasSelectedAndDisabled) {
                        break;
                    }
                }

                // If has a mix of selected and checked but disableddescendant
                if (hasSelectedAndLockedDescendants(childNode)) {
                    return true;
                }

            }

            // If we have both we can return true
            return (hasSelected & hasSelectedAndDisabled);
        }

        @Override
        public boolean isCellEditable(EventObject event) {

            boolean returnValue = false;

            if (event instanceof MouseEvent) {

                MouseEvent mouseEvent = (MouseEvent) event;
                TreePath treePath = tree.getPathForLocation(
                        mouseEvent.getX(), mouseEvent.getY());

                if (treePath != null) {

                    Object node = treePath.getLastPathComponent();
                    if ((node != null) &&
                            (node instanceof DefaultMutableTreeNode)) {
                        DefaultMutableTreeNode treeNode =
                                (DefaultMutableTreeNode) node;
                        Object userObject = treeNode.getUserObject();

                        if (userObject instanceof CheckableResource) {
                            returnValue = true;
                        } else if (userObject instanceof CheckableResource) {
                            returnValue = true;
                        }
                    }
                }
            }
            return returnValue;
        }

        public Component getTreeCellEditorComponent(JTree tree, Object value,
                                                    boolean selected, boolean expanded, boolean leaf, int row) {

            Component rendererComponent = renderer.getTreeCellRendererComponent(
                    tree, value, true, expanded, leaf, row, true);


            ItemListener itemListener = new ItemListener() {

                public void itemStateChanged(ItemEvent itemEvent) {
                    if (stopCellEditing()) {
                        fireEditingStopped();
                    }
                }
            };
            if (rendererComponent instanceof LinkCheckBox) {
                ((LinkCheckBox) rendererComponent).addItemListener(itemListener);
            }

            return rendererComponent;
        }
    }

    public static class CheckableResource implements SelectableResource {

        final static protected Color partialSelectionColor =
                new Color(255, 128, 128);
        protected boolean isParentOfPartiallySelectedChildren = false;
        protected String text;
        protected boolean selected;
        protected ResourceLocator dataResourceLocator;
        protected boolean isEnabled = true;

        public CheckableResource() {
        }

        public CheckableResource(String text, boolean selected,
                                 ResourceLocator dataResourceLocator) {

            this.text = text;
            this.selected = selected;
            this.dataResourceLocator = dataResourceLocator;
        }

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean newValue) {
            selected = newValue;
        }

        public boolean isEnabled() {
            return isEnabled;
        }

        public void setEnabled(boolean value) {
            isEnabled = value;
        }

        public String getText() {
            return text;
        }

        public void setText(String newValue) {
            text = newValue;
        }

        public ResourceLocator getResourceLocator() {
            return dataResourceLocator;
        }

        public void setResourceLocator(ResourceLocator dataResourceLocator) {
            this.dataResourceLocator = dataResourceLocator;
        }

        public boolean isParentOfPartiallySelectedChildren() {
            return isParentOfPartiallySelectedChildren;
        }

        public void setIsParentOfPartiallySelectedChildren(boolean value) {
            this.isParentOfPartiallySelectedChildren = value;
        }

        public Color getBackground() {

            if (isParentOfPartiallySelectedChildren()) {
                return partialSelectionColor;
            } else {
                return Color.WHITE;
            }
        }

        @Override
        public String toString() {
            return text + ":" + selected;
        }
    }


    /**
     *
     */
    static interface SelectableResource extends DataResource {

        public boolean isSelected();

        public void setSelected(boolean newValue);
    }

    /**
     *
     */
    static interface DataResource {

        public ResourceLocator getResourceLocator();

        public void setText(String newValue);

        public String getText();

        public void setEnabled(boolean value);

        public boolean isEnabled();
    }

    /**
     * Expands tree.
     */
    private void expandTree(TreeExpansionFlag expansionFlag,
                            boolean skipSelectingEnabledNodes) {

        TreeNode root = (TreeNode) tree.getModel().getRoot();
        if (expansionFlag == TreeExpansionFlag.EXPAND_SELECTED_ONLY) {

            if (selectedLeafNodePaths.isEmpty()) {
                TreePath rootPath = new TreePath(root);
                expandPath(rootPath);
            } else {
                boolean[] expansionVetoed = {true}; // Default to not expanded
                expandSelectedDescendants(new TreePath(root), true,
                        expansionVetoed, skipSelectingEnabledNodes);
            }
        } else if (expansionFlag == TreeExpansionFlag.EXPAND_ALL) {
            expandAllDescendants(new TreePath(root), true,
                    skipSelectingEnabledNodes);
        } else if (expansionFlag == TreeExpansionFlag.EXPAND_ROOT_ONLY) {
            TreePath rootPath = new TreePath(root);
            expandPath(rootPath);
            checkAllSelectedLeafNodes(rootPath, skipSelectingEnabledNodes);
        }
    }

    /**
     * Expands all children of a tree node.
     */
    private void expandAllDescendants(TreePath parentPath, boolean isExpanding,
                                      boolean skipSelectingEnabledNodes) {

        // Traverse children
        TreeNode node = (TreeNode) parentPath.getLastPathComponent();
        for (Enumeration e = node.children(); e.hasMoreElements(); ) {
            TreeNode n = (TreeNode) e.nextElement();
            TreePath childPath = parentPath.pathByAddingChild(n);
            expandAllDescendants(childPath, isExpanding,
                    skipSelectingEnabledNodes);
        }

        // Expansion or collapse must be done bottom-up
        if (isExpanding) {
            expandPath(parentPath);
        } else {
            collapsePath(parentPath);
        }

        // Leaf nodes processing 
        if (node.isLeaf()) {
            String path = getPath((DefaultMutableTreeNode) node);
            if (selectedLeafNodePaths.contains(path)) {
                manuallySelectNode((DefaultMutableTreeNode) node,
                        skipSelectingEnabledNodes);
            }
        }
    }

    /**
     * Expands all children of a tree node.
     */
    private void expandSelectedDescendants(TreePath parentPath,
                                           boolean isExpanding, boolean[] expansionVetoed,
                                           boolean skipSelectingEnabledNodes) {

        boolean[] expansionIsVetoed = {true}; // true: Defaults to not expanded

        // Traverse children
        TreeNode node = (TreeNode) parentPath.getLastPathComponent();
        for (Enumeration e = node.children(); e.hasMoreElements(); ) {

            DefaultMutableTreeNode childNode =
                    (DefaultMutableTreeNode) e.nextElement();

            // Leaf nodes processing 
            if (childNode.isLeaf()) {

                String path = getPath(childNode);
                if (selectedLeafNodePaths.contains(path)) {
                    manuallySelectNode(childNode, skipSelectingEnabledNodes);
                    expansionIsVetoed[0] = false;
                }
            }
            TreePath childPath = parentPath.pathByAddingChild(childNode);
            expandSelectedDescendants(childPath, isExpanding, expansionVetoed,
                    skipSelectingEnabledNodes);
        }

        if (expansionIsVetoed[0])
            return;

        // Expansion or collapse must be done bottom-up
        if (isExpanding) {
            expandPath(parentPath);
        } else {
            collapsePath(parentPath);
        }
    }

    private String getPath(DefaultMutableTreeNode treeNode) {

        buffer.delete(0, buffer.length());

        TreeNode[] nodesInPath = treeNode.getPath();
        for (Object element : nodesInPath) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) element;
            Object userObject = node.getUserObject();
            if (userObject instanceof CheckableResource) {
                CheckableResource resource = (CheckableResource) userObject;
                buffer.append(resource.getText());

                if (!node.isLeaf()) {
                    buffer.append("/");
                }
            } else if (userObject instanceof CheckableResource) {
                CheckableResource resource = (CheckableResource) userObject;
                buffer.append(resource.getText());

                if (!node.isLeaf()) {
                    buffer.append("/");
                }
            } else {
                buffer.append(userObject.toString());

                if (!node.isLeaf()) {
                    buffer.append("/");
                }
            }
        }

        return buffer.toString();
    }


    private void manuallySelectNode(DefaultMutableTreeNode childNode,
                                    boolean skipSelectingEnabledNodes) {


        Object userObject = childNode.getUserObject();
        if (userObject instanceof CheckableResource) {
            CheckableResource resource = (CheckableResource) userObject;

            // Leave if node is disable
            if (skipSelectingEnabledNodes && resource.isEnabled()) {
                return;
            }

            // Don't select the node if already selected
            if (resource.isSelected())
                return;

            resource.setSelected(true);

            if (childNode.isLeaf()) {
                TreeNode parentNode = childNode.getParent();
                Object parentsUserObject =
                        ((DefaultMutableTreeNode) parentNode).getUserObject();
                if (parentsUserObject instanceof CheckableResource) {
                    CheckableResource parentResource =
                            ((CheckableResource) parentsUserObject);
                    if (!parentResource.isSelected()) {
                        parentResource.setSelected(true);
                    }
                    tree.treeDidChange();
                }
            }
        }
    }

    private void checkAllSelectedLeafNodes(TreePath parentPath,
                                           boolean skipSelectingEnabledNodes) {

        // Traverse children
        TreeNode node = (TreeNode) parentPath.getLastPathComponent();
        for (Enumeration e = node.children(); e.hasMoreElements(); ) {
            TreeNode n = (TreeNode) e.nextElement();
            TreePath childPath = parentPath.pathByAddingChild(n);
            checkAllSelectedLeafNodes(childPath,
                    skipSelectingEnabledNodes);
        }

        // Leaf nodes processing 
        if (node.isLeaf()) {
            String path = getPath((DefaultMutableTreeNode) node);
            if (selectedLeafNodePaths.contains(path)) {
                manuallySelectNode((DefaultMutableTreeNode) node,
                        skipSelectingEnabledNodes);
            }
        }
    }

    private void collapsePath(TreePath treePath) {
        if (!tree.isCollapsed(treePath)) {
            tree.collapsePath(treePath);
        }
    }

    private void expandPath(TreePath treePath) {
        if (!tree.isExpanded(treePath)) {
            tree.expandPath(treePath);
        }
    }

    public boolean hasSelectedChildren(TreeNode treeNode) {

        Enumeration children = treeNode.children();
        while (children.hasMoreElements()) {

            TreeNode childNode = (TreeNode) children.nextElement();

            Object childsUserObject =
                    ((DefaultMutableTreeNode) childNode).getUserObject();
            if (childsUserObject instanceof CheckableResource) {
                CheckableResource childResource =
                        ((CheckableResource) childsUserObject);
                if (childResource.isSelected()) {
                    return true;
                }
                tree.treeDidChange();
            }
        }
        return false;
    }

    static private Set<ResourceLocator> getLoadedResources() {
        Set<ResourceLocator> loadedResources = IGV.getInstance().getDataResourceLocators();
        loadedResources.addAll(AttributeManager.getInstance().getLoadedResources());
        return loadedResources;
    }

    /**
     * This method will only check tree items.
     */
    private void recheckTree() {

        if (leafNodeMap != null && !leafNodeMap.isEmpty()) {
            Collection<TreeNode> leaves = leafNodeMap.values();
            for (TreeNode leafNode : leaves) {
                TreeNode parent = leafNode.getParent();
                if (parent != null) {

                    Object userObject =
                            ((DefaultMutableTreeNode) leafNode).getUserObject();
                    CheckableResource checkableLeafResource =
                            ((CheckableResource) userObject);
                    if (userObject != null) {

                        if (checkableLeafResource.isSelected()) {
                            checkNode(parent, true);
                            while (true) {
                                parent = parent.getParent();
                                if (parent == null) {
                                    break;
                                }
                                checkNode(parent, true);
                            }
                        }
                    }
                }
            }
        }
    }

    protected void checkNode(TreeNode node, boolean checked) {

        // Only allowed to check
        if (!checked)
            return;

        Object userObject = ((DefaultMutableTreeNode) node).getUserObject();
        CheckableResource nodeResource = null;
        if (userObject instanceof CheckableResource) {
            nodeResource = ((CheckableResource) userObject);
            nodeResource.setSelected(checked);
        }
    }

}
