package usda.weru.util;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;

import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.apache.log4j.Logger;

/**
 *
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public class LazyLoadingTreeController implements TreeWillExpandListener {

    private final static Logger LOGGER = Logger.getLogger(LazyLoadingTreeController.class);
    private final JTree c_tree;
    private ModelAdapter c_model;

    /**
     *
     * @param tree
     */
    public LazyLoadingTreeController(JTree tree) {
        c_tree = tree;
        setModel(c_tree.getModel());

        c_tree.addPropertyChangeListener(JTree.TREE_MODEL_PROPERTY, new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                setModel((TreeModel) evt.getNewValue());
            }
        });

        c_tree.addTreeWillExpandListener(this);
    }

    /**
     *
     * @param model
     */
    public void setModel(TreeModel model) {
        c_model = null;

        if (model instanceof ModelAdapter) {
            c_model = (ModelAdapter) model;
        } else if (model instanceof DefaultTreeModel) {
            c_model = new DefaultModelAdapter((DefaultTreeModel) model);
        } else if (model == null) {
        } else {
            throw new IllegalArgumentException("TreeModel not supported.  "
                    + "Implement LazyLoadingTreeController.ModelAdapter or extend DefaultTreeModel.");
        }
    }

    /**
     *
     * @param event
     * @throws ExpandVetoException
     */
    @Override
    public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
        TreePath path = event.getPath();
        Object lastPathComponent = path.getLastPathComponent();
        if (lastPathComponent instanceof LazyLoadingTreeNode) {
            LazyLoadingTreeNode lazyNode = (LazyLoadingTreeNode) lastPathComponent;
            expandNode(lazyNode);
        }

    }

    private void expandNode(LazyLoadingTreeNode node) {
        if (node.getChildrenLoaded()) {
            //nodes are already loaded
            return;
        }

        //check if we're already loading this node
        if (node.getChildCount() == 1 && node.getChildAt(0) instanceof LoadingNode) {
            //in the process of loading.
            return;
        }

        //add a loading node
        setNodeChildrenSwing(node, createLoadingNode());
        SwingWorker<MutableTreeNode[], MutableTreeNode> worker = new LazyWorker(node);
        worker.execute();

    }

    private MutableTreeNode createLoadingNode() {
        return new LoadingNode();
    }

    /**
     *
     * @param event
     * @throws ExpandVetoException
     */
    @Override
    public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
    }

    //TODO: maybe move these into a ModelAdapter interface?
    /**
     * 
     * @param node
     * @param children
     */
    protected void setNodeChildren(LazyLoadingTreeNode node, MutableTreeNode... children) {
        //synchronized in case multiple threads are attempting to update the node
        synchronized (node) {
            while (node.getChildCount() > 0) {
                node.remove(0);
            }
            for (int i = 0; children != null && i < children.length; i++) {
                node.insert(children[i], i);
            }
        }
    }

    /**
     *
     * @param node
     * @param children
     */
    public void setNodeChildrenSwing(LazyLoadingTreeNode node, MutableTreeNode... children) {
        setNodeChildren(node, children);
        nodeStructureChanged(node);
    }

    /**
     * This method should restore the Node initial state if the worker if canceled
     * @param node
     */
    protected void resetNode(LazyLoadingTreeNode node) {
        synchronized (node) {
            while (node.getChildCount() > 0) {
                node.remove(0);
            }
        }
    }

    /**
     *
     * @param node
     */
    public void resetNodeSwing(LazyLoadingTreeNode node) {
        resetNode(node);
        nodeStructureChanged(node);
    }

    private void nodeStructureChanged(TreeNode node) {
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//        assert SwingUtilities.isEventDispatchThread() : "Must be executed on event queue.";

        //tell the model we've changed things
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//       assert c_model != null : "No tree model provided";
        c_model.nodeStructureChanged(node);
    }

    /**
     * Swing worker that loads the children in a background thread and then sets them
     * on the node with the event queue
     */
    private class LazyWorker extends SwingWorker<MutableTreeNode[], MutableTreeNode> {

        private final LazyLoadingTreeNode c_node;

        public LazyWorker(LazyLoadingTreeNode node) {
            c_node = node;
        }

        @Override
        protected MutableTreeNode[] doInBackground() throws Exception {
            MutableTreeNode[] nodes = c_node.loadChildren();

            c_node.setAllowsChildren(nodes == null || nodes.length > 0);
            setNodeChildren(c_node, nodes);
            return nodes;
        }

        @Override
        protected void done() {
            try {
                MutableTreeNode[] nodes = get();
                if (nodes == null) {
                    List<TreeNode> nodesForPath = new LinkedList<TreeNode>();
                    TreeNode i = c_node;
                    while (i != null) {
                        nodesForPath.add(0, i);
                        i = i.getParent();
                    }
                    TreePath path = new TreePath(nodesForPath.toArray());
                    c_tree.collapsePath(path);
                }
                nodeStructureChanged(c_node);
            } catch (InterruptedException | ExecutionException e) {
                resetNodeSwing(c_node);
                LOGGER.warn("Execution e.", e);
            }
        }
    }

    /**
     *
     */
    public interface ModelAdapter {

        /**
         *
         * @param node
         */
        public void nodeStructureChanged(TreeNode node);
    }

    private class DefaultModelAdapter implements ModelAdapter {

        private final DefaultTreeModel c_internalModel;

        public DefaultModelAdapter(DefaultTreeModel model) {
            c_internalModel = model;
        }

        @Override
        public void nodeStructureChanged(TreeNode node) {
            c_internalModel.nodeStructureChanged(node);
        }

    }

    /**
     *
     */
    public class LoadingNode extends DefaultMutableTreeNode {

        private static final long serialVersionUID = 1L;

        /**
         *
         */
        public LoadingNode() {
            super("Loading...", false);
        }
    }
}
