package usda.weru.util.tree;

import java.awt.Cursor;
import static java.awt.Cursor.getPredefinedCursor;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ExecutionException;
import javax.swing.JTree;
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.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 

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

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


    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);
    }

    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.");
        }
    }

    @Override
    public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
        TreePath path = event.getPath();
        Object lastPathComponent = path.getLastPathComponent();
        if (lastPathComponent instanceof LazyLoadingTreeNode) {
            // MEH : added so that the CSIP Lmod cache file item will update itself on the fly,
            //       whenever it is expanded.  Since files will constantly be being added...
            if (lastPathComponent instanceof WepsTreeNodeFile) {
                if (((WepsTreeNodeFile)lastPathComponent).getNodeData().getPath().contains("filesCache/")) {
                    ((WepsTreeNodeFile)lastPathComponent).refreshChildren();
                    c_model.nodeStructureChanged(((WepsTreeNodeFile)lastPathComponent));
                }
            }
            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;
        }

        //otherwise add a (default) loading node
        setNodeChildrenSwing(node, createLoadingNode());
        if (!(node instanceof WepsTreeNodeCrLmod)) {
            SwingWorker<MutableTreeNode[], MutableTreeNode> worker = new LazyWorker(node, c_tree);
            worker.execute();
        }
    }

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

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

    //TODO: maybe move these into a ModelAdapter interface?
    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);
            }
            if (node.getAllowsChildren()) {
                for (int i = 0; children != null && i < children.length; i++) {
                    node.insert(children[i], i);
                }
            } else {
                LOGGER.warn("LazyLoadingTreeController: setNodeChildren called, but node does not allow children.");                
            }
        }
    }

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

    protected void resetNode(LazyLoadingTreeNode node) {
        synchronized (node) {
            while (node.getChildCount() > 0) {
                node.remove(0);
            }
        }
    }

    public void resetNodeSwing(LazyLoadingTreeNode node) {
        resetNode(node);
        nodeStructureChanged(node);
    }

    private void nodeStructureChanged(TreeNode node) {
        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 worker_node;
        private final JTree worker_tree;
        private WepsTreeComboBox worker_combobox;
        private Cursor cursorSave;

        public LazyWorker(LazyLoadingTreeNode node, JTree tree) {
            worker_node = node;
            worker_tree = tree;
            worker_combobox = null;
            cursorSave = null;
            
            if (worker_tree.getModel() instanceof WepsTreeModel) {
                WepsTreeModel model = ((WepsTreeModel)worker_tree.getModel());
                if (model.useWaitCursor()) {
                    worker_combobox = model.getTreeCombo();
                    if (worker_combobox != null && worker_combobox.isPopupVisible()) {
                        cursorSave = worker_combobox.getCursor();
                        worker_combobox.setCursor(getPredefinedCursor(Cursor.WAIT_CURSOR));
                    }
                }
            }
        }

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

            nodes = (nodes != null && nodes.length <= 0) ? null : nodes;
            if (nodes != null) {
                worker_node.setAllowsChildren(true);
                setNodeChildren(worker_node, nodes);
            } else {
                setNodeChildren(worker_node, new MutableTreeNode[0]);
                worker_node.setAllowsChildren(false);
            }
            return nodes;
        }

        @Override
        protected void done() {
            try {
                MutableTreeNode[] nodes = get();
// No longer required.
// Taken care above by removing all chldren
//                if (nodes == null) {
//                    List<TreeNode> nodesForPath = new LinkedList<TreeNode>();
//                    TreeNode i = worker_node;
//                    while (i != null) {
//                        nodesForPath.add(0, i);
//                        i = i.getParent();
//                    }
//                    TreePath path = new TreePath(nodesForPath.toArray());
//                    worker_tree.collapsePath(path);
//                }
                
                nodeStructureChanged(worker_node);
            } catch (InterruptedException | ExecutionException e) {
                resetNodeSwing(worker_node);
                LOGGER.warn("Execution e.", e);
            }
            if (cursorSave != null) {
                worker_combobox.setCursor(cursorSave);
            }
        }
    }

    public interface ModelAdapter {
        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);
            c_internalModel.reload(node);
        }

    }

    public class LoadingNode extends DefaultMutableTreeNode {

        private static final long serialVersionUID = 1L;

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