
/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.osedu.org/licenses/ECL-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */


/*
 * VueDragTree.java
 *
 * Created on May 5, 2003, 4:08 PM
 */
package tufts.vue;

import tufts.Util;
import tufts.vue.gui.GUI;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.util.*;
import javax.swing.event.*;
import osid.dr.*;

import osid.filing.*;
import tufts.oki.remoteFiling.*;
import tufts.oki.localFiling.*;


import javax.swing.tree.*;
import java.util.Iterator;

/**
 *
 * @version $Revision: 1.83 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $
 * @author  rsaigal
 * @author  Scott Fraize
 */
public class VueDragTree extends JTree
    implements DragGestureListener,
               DragSourceListener,
               TreeSelectionListener,
               ActionListener
{
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(VueDragTree.class);
    
    public static ResourceNode oldnode;
    
    private static final int DOUBLE_CLICK = 2;

    //private static final boolean FullStartup = VueUtil.isMacPlatform() && !DEBUG.Enabled;
    private static final boolean FullStartup = true;
    //private static final boolean SlowStartup = false;
    
//     protected static final ImageIcon nleafIcon = VueResources.getImageIcon("favorites.leafIcon") ;
//     protected static final ImageIcon inactiveIcon = VueResources.getImageIcon("favorites.inactiveIcon") ;
//     protected static final ImageIcon activeIcon = VueResources.getImageIcon("favorites.activeIcon") ;

    private final java.util.List<Resource> mResources = new ArrayList();

    public VueDragTree(Iterable<Resource> iterable, String treeName) {
        if (DEBUG.DR) Log.debug("NEW: " + treeName + "; " + Util.tags(iterable));
        //if (DEBUG.Enabled) Util.printStackTrace(Util.tags(this) + "; NEW " + getClass() + " " + treeName);
        setModel(createTreeModel(iterable, treeName));
        setName(treeName);
        setRootVisible(true);
        if (FullStartup) {
            this.expandRow(0);
            this.expandRow(1);
            //this.setRootVisible(false);
        }
        implementDrag(this);
        createPopupMenu();
        this.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        addTreeSelectionListener(this);
        addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent me){
                if  (me.getClickCount() != DOUBLE_CLICK)
                    return;
                TreePath path = getPathForLocation(me.getX(), me.getY());
                if (path == null)
                    return;
                Object c = path.getLastPathComponent();
                if (c instanceof CabinetNode) {
                    CabinetNode cabNode = (CabinetNode) path.getLastPathComponent();
                    Resource r = cabNode.getResource();
                    if (r != null)
                        r.displayContent();
//                     Object uo = cabNode.getUserObject();
//                     if (uo instanceof Resource)
//                         ((Resource)uo).displayContent();
                }
            }
        });

        addListeners();

        if (DEBUG.SELECTION) Util.printStackTrace(GUI.namex(this) + " constructed from obj " + iterable + " treeName " + treeName);

    }
    
    protected VueDragTree(FavoritesNode favoritesNode) {

        //Util.printStackTrace("NEW: " + getClass() + "; " + favoritesNode + "; " + favoritesNode.getResource());
        if (DEBUG.Enabled) Log.debug("NEW: " + favoritesNode + "; " + favoritesNode.getResource());;

        setName(favoritesNode.toString());
        setModel(new DefaultTreeModel(favoritesNode));
        expandRow(0);
        createPopupMenu();
        implementDrag(this);
        addTreeSelectionListener(this);
        addListeners();

        if (DEBUG.SELECTION) Util.printStackTrace(GUI.namex(this) + " constructed from FavoritesNode " + favoritesNode);
        
    }

    protected void addListeners() {
        VUE.addActiveListener(Resource.class, this);
    }


    @Override
    public void addNotify() {
        super.addNotify();
        
//         if (resourceSelection == null) {
//             // Only do this on addNotify, as workaround for buggy multiple
//             // instances of data sources being spuriously generated by
//             // the data source loader.
//             resourceSelection = VUE.getResourceSelection();
//             resourceSelection.addListener(this);
//         }

        
        if (DEBUG.SELECTION) System.out.println(GUI.namex(this) + " addNotify");
    }
    
    @Override
    public void removeNotify() {
        super.removeNotify();
        if (DEBUG.SELECTION) System.out.println(GUI.namex(this) + " removeNotify");
    }

    public void activeChanged(ActiveEvent e, Resource r) {
        // as we won't see events from ourself, we can always clear the selection
        clearSelection();
        repaint();
    }

    
//     /** ResourceSelection.Listener */
//     public void resourceSelectionChanged(tufts.vue.ResourceSelection.Event e) {
//         if (e.source == this) {
//             return;
//         //if (getPicked() == e.selected) {
//         //    ; // do nothing; already selected
//         } else {
//             // todo: if contains selected item, select it
//             // TODO: clearing the selection isn't working! don't know
//             // if is just a repaint issue.
//             clearSelection();
//             //setSelectionRow(-1);
//             repaint();
//         }
//     }
    
    private void  implementDrag(VueDragTree tree){
        DragSource dragSource = DragSource.getDefaultDragSource();
        dragSource.createDefaultDragGestureRecognizer
            (tree,
             DnDConstants.ACTION_COPY |
             DnDConstants.ACTION_MOVE |
             DnDConstants.ACTION_LINK,
             (DragGestureListener) tree);
        
        addTreeExpansionListener(new TreeExpansionListener(){
            public void treeCollapsed(TreeExpansionEvent e) {}
            public void treeExpanded(TreeExpansionEvent e) {
                TreePath path = e.getPath();
                if(path != null) {
                    if (path.getLastPathComponent() instanceof FileNode){
                        FileNode node = (FileNode)path.getLastPathComponent();
                        if( !node.isExplored()) {
                            DefaultTreeModel model =
                                    (DefaultTreeModel)getModel();
                            node.explore();
                            model.nodeStructureChanged(node);
                        }
                    }
                }
            }
        });
        //  Add a tree will expand listener.
        addTreeWillExpandListener(new TreeWillExpandListener() {
            public void treeWillExpand(TreeExpansionEvent e) {
                TreePath path = e.getPath();
                if (path.getLastPathComponent() instanceof CabinetNode) {
                    CabinetNode cabNode = (CabinetNode) path.getLastPathComponent();
                    if (cabNode == null) return;
                    setSelectionPath(path);
                    if (cabNode.getCabinet() != null)cabNode.getDataModel().reload();
                }
            }
            public void treeWillCollapse(TreeExpansionEvent e) {}
        });
        VueDragTreeCellRenderer renderer = new VueDragTreeCellRenderer(this);
        tree.setCellRenderer(renderer);
        
        ToolTipManager.sharedInstance().registerComponent(tree);
    }
    
    
    
    
    private DefaultTreeModel createTreeModel(Iterable<Resource> iterable, String treeName) {
        //final Resource resourceRoot = Resource.getFactory().get(treeName);
        //final ResourceNode root = new ResourceNode(resourceRoot);
        final ResourceNode root = new RootNode(treeName);
        if (DEBUG.Enabled) Log.debug("createTreeModel: " + root);
        
        if (iterable != null) {
            boolean didFirst = false;
            for (Resource resource : iterable) {
                mResources.add(resource);
                if (DEBUG.RESOURCE || DEBUG.DR) Log.debug("\tchild: " + resource);

                if (resource instanceof CabinetResource) {
                    CabinetResource cabRes = (CabinetResource) resource;
                    CabinetEntry entry = cabRes.getEntry();
                    CabinetNode cabNode = null;
                    if (entry instanceof RemoteCabinetEntry)
                        cabNode = new CabinetNode(cabRes, CabinetNode.REMOTE);
                    else
                        cabNode = new CabinetNode(cabRes, CabinetNode.LOCAL);
                    
                    //-------------------------------------------------------
                    root.add(cabNode);
                    //-------------------------------------------------------

                    if (FullStartup && !didFirst) {
                        // Do NOT DO THIS AUTOMATICALLY -- it can dramaticaly slow startup times.
                        // by tens of seconds (!) -- SMF 2007-10-10
                        if ((new File(cabRes.getSpec())).isDirectory())
                            cabNode.explore();
                        else if (cabNode.getCabinet() != null)
                            cabNode.explore();
                    }
                } else {
                    ResourceNode node = new ResourceNode(resource);
                    
                    //-------------------------------------------------------
                    root.add(node);
                    //-------------------------------------------------------
                }
                didFirst = true;
            }
        }
        return new DefaultTreeModel(root);
    }

    //****************************************
    
    public void dragGestureRecognized(DragGestureEvent e) {
        
        if (getSelectionPath() != null) {
            TreePath path = getLeadSelectionPath();
            oldnode = (ResourceNode)path.getLastPathComponent();
            ResourceNode parentnode = (ResourceNode)oldnode.getParent();
            Resource resource = oldnode.getResource();
            
            if (DEBUG.DND) System.out.println(this + " dragGestureRecognized " + e);
            if (DEBUG.DND) System.out.println("selected node is " + oldnode.getClass() + "[" + oldnode + "] resource=" + resource);

            if (resource != null) 
                GUI.startRecognizedDrag(e, resource, this);
            
        }
    }
    
    
    public void dragDropEnd(DragSourceDropEvent e) {
        if (tufts.vue.VUE.dropIsLocal == true){
            DefaultTreeModel model = (DefaultTreeModel)this.getModel();
            model.removeNodeFromParent(oldnode);
            tufts.vue.VUE.dropIsLocal = false;
        }
    }
    
    public void dragEnter(DragSourceDragEvent e) { }
    public void dragExit(DragSourceEvent e) {}
    public void dragOver(DragSourceDragEvent e) {}
    public void dropActionChanged(DragSourceDragEvent e) {
        if (DEBUG.DND) System.out.println("VueDragTree: dropActionChanged  to  " + tufts.vue.gui.GUI.dropName(e.getDropAction()));
    }
    
    
    public void valueChanged(TreeSelectionEvent e) {
        try {
            if (e.isAddedPath() && e.getPath().getLastPathComponent() != null ) {
                Resource resource = (Resource)((ResourceNode)e.getPath().getLastPathComponent()).getResource();
                VUE.setActive(Resource.class, this, resource);
                //resourceSelection.setTo(resource, this);
            }
        } catch(Exception ex) {
            // (null,ex.toString(),"Error in VueDragTree Selection");
            System.out.println("VueDragTree.valueChanged "+ex.getMessage());
            ex.printStackTrace();
        }
    }
    
    public void createPopupMenu() {
        JMenuItem menuItem;
        
        //Create the popup menu.
        JPopupMenu popup = new JPopupMenu();
        menuItem = new JMenuItem(VueResources.getString("vuedragtree.menu.openresource"));
        menuItem.addActionListener(this);
        popup.add(menuItem);
        if (mResources.size() > 0) {
            menuItem = new JMenuItem(VueResources.getString("vuedragtree.menu.addalltomap"));
            menuItem.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        VUE.getActiveViewer().getMapDropTarget()
                            .processTransferable(new tufts.vue.gui.GUI.ResourceTransfer(mResources), null);
                        // VUE.getActiveMap().getUndoManager().mark("Add Resources to Map"); // todo: can't override pre-existing "Drop" mark
                    }
                });
        }
                
        popup.add(menuItem);
        
        //Add listener to the text area so the popup menu can come up.
        MouseListener popupListener = new PopupListener(popup);
        this.addMouseListener(popupListener);
    }
    
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() instanceof JMenuItem){
            JMenuItem source = (JMenuItem)(e.getSource());
            TreePath tp = this.getSelectionPath();
            
            if (tp != null){
                ResourceNode resNode = (ResourceNode)tp.getLastPathComponent();
                resNode.getResource().displayContent();
            }
        }
    }
    
    class PopupListener extends MouseAdapter {
        JPopupMenu popup;
        PopupListener(JPopupMenu popupMenu) {
            popup = popupMenu;
        }
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }
        
        public void mouseClicked(MouseEvent e) {
            maybeShowPopup(e);
        }
        
        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }
        
        private void maybeShowPopup(MouseEvent e) {
            if (VueDragTree.this.getSelectionPath() != null){
                if (e.isPopupTrigger()) {
                    
                    
                    popup.show(e.getComponent(),
                            e.getX(), e.getY());
                    
                }
            }
        }
    }


    class VueDragTreeCellRenderer extends DefaultTreeCellRenderer{

        protected final VueDragTree tree;
        
        public VueDragTreeCellRenderer(VueDragTree vdTree) {
            this.tree = vdTree;
            //if (DEBUG.Enabled) setOpaque(true);
        }

        protected final void superGetTreeCellRendererComponent(
                JTree tree,
                Object value,
                boolean sel,
                boolean expanded,
                boolean leaf,
                int row,
                boolean hasFocus)
        {
            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);

            if (VUE.VUE3) colorLabel(sel, value);
            
        }

        private void colorLabel(boolean selected, Object value) {
            
            final ResourceNode node = (ResourceNode) value;
                
            if (!selected && node.resource != null && node.resource.equals(VUE.getActiveResource()))
                setForeground(Color.blue);
              //setForeground(VueConstants.COLOR_SELECTION);
            else
                setForeground(Color.black);
        }
        
        
        public Component getTreeCellRendererComponent(
                JTree tree,
                Object value,
                boolean sel,
                boolean expanded,
                boolean leaf,
                int row,
                boolean hasFocus) {
            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);

            if (leaf) {
                ResourceNode node = (ResourceNode) value;
                Icon icon = node.getIcon();
                if (icon != null)
                    setIcon(icon);
                if (VUE.VUE3) colorLabel(sel, value);
            }

            
            return this;
        }
                
    }

    @Override
    public String toString() {
        return getClass().getName() + "[" + getName() + "]";
    }

    
}


class ResourceNode extends DefaultMutableTreeNode {
    protected Resource resource;
    
    protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ResourceNode.class);

    public ResourceNode(Resource resource) {
        this.resource = resource;
        //if (resource != null) resource.setDebugProperty("TREE-NODE", getClass().getSimpleName());
        setUserObject(resource);
    }

    protected ResourceNode() {
        resource = null;
    }

    public Resource getResource() {
        return resource;
    }
    
    public String toString() {
        String title = resource.getTitle();
        if (title == null || title.length() == 0)
            return resource.getSpec();
        else
            return title;
    }
    
    public Icon getIcon() {
        if (resource != null) {
            //resource.setDebugProperty("TREE-LEAF", isLeaf() ? "TRUE" : "FALSE");
            return resource.getTinyIcon();
        } else {
            return null;
        }
    }
}
    

class CabinetNode extends ResourceNode {

    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(CabinetNode.class);

    public static CabinetNode getCabinetNode(String title, File file, ResourceNode rootNode, DefaultTreeModel model){
        CabinetNode node= null;
        try{
            osid.shared.Agent agent = null;
            LocalCabinetEntry cab;
            if(file.isDirectory()){
                cab = LocalCabinet.instance(file.getAbsolutePath(),agent,null);
            } else {
                
                // todo: use factory, also -- should this be a bytestore?
                
                // TODO: BUG: THIS IS COMPLETELY ILLEGAL (in fact, LocalCabinetEntry
                // should be an abstract class)
                // SMF 2008-04-17
                
                cab = new LocalCabinetEntry(file.getAbsolutePath(),agent,null);
                if (DEBUG.Enabled) Log.debug("new INDETERMINATE CABINET ENTRY " + cab);
            }
            CabinetResource res = CabinetResource.create(cab);
            //if (DEBUG.Enabled) Log.debug("new " + res);
            CabinetEntry entry = res.getEntry();

            //if (title != null) res.setTitle(title);

            if (entry instanceof RemoteCabinetEntry)
                node =  new CabinetNode(res, REMOTE);
            else
                node = new CabinetNode(res, LOCAL);
            model.insertNodeInto(node, rootNode, (rootNode.getChildCount()));
            //node.explore();
        } catch(Exception ex){
            System.out.println("CabinetNode: "+ex);
            ex.printStackTrace();
        }
        return node;
    }
    
    public static final String LOCAL = "local";
    public static final String REMOTE = "remote";
    
    private final String type;
    private final boolean isLeaf;
    private final CabinetEntry entry;
    
    private boolean explored = false;
    
    CabinetNode(final CabinetResource r, String type) {
        super(r);
        this.type = type;
        this.entry = r.getEntry();

        if (r == null) {
            isLeaf = false;
        } else if (r.getClientType() == Resource.FILE) {
            isLeaf = true;
        } else if (r.getClientType() == Resource.DIRECTORY) {
            isLeaf = false;
        } else if (entry != null) {
            if (type.equals(REMOTE) && ((RemoteCabinetEntry)entry).isCabinet())
                isLeaf = false;
            else if (type.equals(LOCAL) && ((LocalCabinetEntry)entry).isCabinet())
                isLeaf = false;
            else
                isLeaf = true;
        } else
            isLeaf = true;

        //setAllowsChildren(!isLeaf);
        
    }

//     public CabinetNode(final CabinetResource cabRes) {
//         super(cabRes);
//         this.type = LOCAL;
//         this.entry = cabRes.getEntry();
//         this.isLeaf = false;
//     }

    // Will allow lazy creation of the Resource (including exact app icon fetch -- helps much on Windows)
    private static final boolean LazyResources = false;
    

    private CabinetNode(final LocalCabinetEntry lentry) {
        super(LazyResources ? null : CabinetResource.create(lentry));
        this.type = LOCAL;
        this.entry = lentry;
        this.isLeaf = !lentry.isCabinet();
        //setAllowsChildren(!isLeaf);
        
    }    
    private CabinetNode(final RemoteCabinetEntry rentry) {
        super(LazyResources ? null : CabinetResource.create(rentry));
        this.type = REMOTE;
        this.entry = rentry;
        this.isLeaf = !rentry.isCabinet();
        //setAllowsChildren(!isLeaf);
    }    

    @Override
    public final Resource getResource() {
        if (LazyResources) {
            synchronized (this) {
                if (resource == null)  {
                    //Util.printStackTrace("producing for " + entry);
                    resource = CabinetResource.create(entry);
                }
            }
        }
        return resource;
    }
    
    public final boolean isLeaf() {
        return isLeaf;
    }

//     public final Icon getIcon() {
//         if (isLeaf && resource != null)
//             return resource.getTinyIcon();
//         else
//             return null;
//     }
    
    /**
     *  Return the cabinet entry associated with this tree node.  If it is a cabinet,
     *  then return it.  Otherwise, return null.
     */
    public Cabinet getCabinet() {
        return entry instanceof Cabinet ? ((Cabinet)entry) : null;
    }
    
    /*
     *  Expand the tree (ie. find the cabinet entries below this node).
     *  This only applies if the current node is a cabinet.
     */
    public void explore() {
        if (explored)
            return;

        if (getCabinet() != null) {

            final String name = Util.tags(entry);
            
            try {
                if (DEBUG.Enabled) Log.debug(name + ": explore...");
                loadEntries();
                if (DEBUG.Enabled) Log.debug(name + ": explored.");
            } catch (FilingException e) {
                Log.warn("explore: " + name + ";", e);
            }
        } 
        
    }

    private void loadEntries()
        throws FilingException
    {
        if (type.equals(LOCAL)) {
            
            CabinetEntryIterator i = (LocalCabinetEntryIterator) getCabinet().entries();
                    
            while (i.hasNext()) {
                LocalCabinetEntry ce = (LocalCabinetEntry) i.next();
                if (ce.getDisplayName().startsWith(".")) // don't display dot files
                    continue;
                this.add(new CabinetNode(ce));
            }
            this.explored = true;

        } else if (type.equals(REMOTE)) {
            
            CabinetEntryIterator i = (RemoteCabinetEntryIterator) getCabinet().entries();
            
            while (i.hasNext()) {
                RemoteCabinetEntry ce = (RemoteCabinetEntry) i.next();
                if (ce.getDisplayName().startsWith(".")) // don't display dot files
                    continue;
                this.add(new CabinetNode(ce));
            }
        }
        
    }
    
    /**
     *  Return a string version of the node.  In this implementation, the display name
     *  of the cabinet entry is returned.
     */
    @Override
    public String toString() {

        if (resource != null) {
            return resource.getTitle();
        } else if (entry != null) {
            try {
                return entry.getDisplayName();
            } catch (Throwable t) {
                Log.warn(t);
                return entry.toString();
            }
        } else
            return String.format("%s@%x(%s)", getClass().getName(), System.identityHashCode(this), type);
    }
    
    /**
     *  Return the data model used.
     */
    DefaultTreeModel getDataModel() {
        explore();
        return new DefaultTreeModel(this);
    }
}



class FavoritesNode extends ResourceNode {
    private boolean explored = false;
    public FavoritesNode(Resource resource){
        super(resource);
        // ensure is marked as favorites (some versions of VUE may have left marked as type NONE)
        resource.setClientType(Resource.FAVORITES);
    }

    public boolean isExplored() { return explored; }
}

class RootNode extends ResourceNode {
    final String name;
    public RootNode(String name) {
        super(null);
        this.name = name;
    }

    @Override
    public final String toString() {
        return name;
    }

    @Override
    public final boolean isLeaf() {
        return false;
    }
}



class FileNode extends ResourceNode {
    private boolean explored = false;
    public FileNode(File file) 	{
        setUserObject(file);
        if (DEBUG.Enabled) Log.debug("NEW FileNode: " + file);
        
        // Code w/no apparent effect commented out -- SMF 2007-10-05
        //try{
        //    MapResource resource = new  MapResource(file.toURL().toString());
        //}catch (Exception ex){};
        
    }
    public boolean getAllowsChildren() { return isDirectory(); }
    public boolean isLeaf() 	{ return !isDirectory(); }
    public File getFile()		{ return (File)getUserObject(); }
    public boolean isExplored() { return explored; }
    public boolean isDirectory() {
        File file = getFile();
        if (file != null) {
            return file.isDirectory();
        } else {
            return false;
        }
    }
    public String toString() {
        File file = (File)getUserObject();
        String filename = file.toString();
        int index = filename.lastIndexOf(File.separator);
        
        return (index != -1 && index != filename.length()-1) ?
            filename.substring(index+1) :
            filename;
    }
    
    public void displayContent(){
        
        
        try{
            URL url = getFile().toURL();
            VueUtil.openURL(url.toString().replaceFirst("/",""));
        }catch (Exception ex){System.out.println("problem opening conten");}
    }
    
    
    public void explore() {
        
        if(!isDirectory())
            return;
        
        if(!isExplored()) {
            File file = getFile();
            File[] contents = file.listFiles();
            
            for(int i=0; i < contents.length; ++i)
                add(new FileNode(contents[i]));
            
            explored = true;
        }
    }
}


