package org.freehep.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileSystemView;
import javax.swing.filechooser.FileView;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

/**
 * A class which allows a user to select directories or files, similar to JFileChooser,
 * except that it display files as a tree, and is better suited to selecting directories
 * than the current file chooser (see java bug id 4239219).
 *
 * TODO: Understand issue with moving mouse between double clicks
 * (seems like MOUSE_PRESSED and MOUSE_RELEASED events are generated, but not MOUSE_CLICKED)
 * See Bug ID 4218549
 *
 * @author Tony Johnson (tony_johnson@slac.stanford.edu)
 * @version $Id: JDirectoryChooser.java 8584 2006-08-10 23:06:37Z duns $
 */
public class JDirectoryChooser extends JComponent
{
   /**
    * Create a JDirectoryChooser with the default FileSystemView
    */
   public JDirectoryChooser()
   {
      this(FileSystemView.getFileSystemView());
   }
   /**
    * Create a JDirectoryChooser with the default FileSystemView
    * @param currentDirectory The directory to which the tree is initially set
    */
   public JDirectoryChooser(File currentDirectory)
   {
      this();
      setCurrentDirectory(currentDirectory);
   }
   /**
    * Create a JDirectoryChooser with the default FileSystemView
    * @param currentDirectory The directory to which the tree is initially set
    */
   public JDirectoryChooser(String currentDirectory)
   {
      this();
      if (currentDirectory != null) setCurrentDirectory(new File(currentDirectory));
   }
   /**
    * Create a JDirectoryChooser
    * @param currentDirectory The directory to which the tree is initially set
    * @param view The FileSystemView to use
    */
   public JDirectoryChooser(File currentDirectory, FileSystemView view)
   {
      this(view);
      setCurrentDirectory(currentDirectory);
   }
   /**
    * Create a JDirectoryChooser
    * @param currentDirectory The directory to which the tree is initially set
    * @param view The FileSystemView to use
    */
   public JDirectoryChooser(String currentDirectory, FileSystemView view)
   {
      this(view);
      setCurrentDirectory(new File(currentDirectory));
   }
   /**
    * Create a JDirectoryChooser
    * @param view The FileSystemView to use
    */
   public JDirectoryChooser(FileSystemView view)
   {
      setup(view);
      ButtonListener al = new ButtonListener();
      
      tree = new JTree();
      tree.setRootVisible(false);
      tree.setShowsRootHandles(true);
      tree.setCellRenderer(new FileRenderer());
      tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
      tree.addTreeSelectionListener(al);
      
      approve.addActionListener(al);
      cancel.addActionListener(al);
      approve.setEnabled(false);
      
      setLayout(new BorderLayout());
      add(new JScrollPane(tree));
      
      JPanel p = new JPanel(new FlowLayout());
      p.add(approve);
      p.add(cancel);
      add(p,BorderLayout.SOUTH);
   }
   public void addNotify()
   {
      tree.setModel(model);
      makeCurrentDirectoryVisible();
      super.addNotify();
   }
   private void makeCurrentDirectoryVisible()
   {
      if (currentDirectory != null)
      {
         File dir = currentDirectory;
         FileSystemView view = foo.getFileSystemView();
         Vector v = new Vector();
         while (true)
         {
            if (dir == null) return; // Something wrong, ignore dir
            v.addElement(dir);
            if (view.isRoot(dir)) break;
            dir = view.getParentDirectory(dir);
         }
         Object[] files = new Object[v.size()+1];
         Object node = model.getRoot();
         files[0] = node;
         for (int i=1; i<files.length; i++)
         {
            File here = (File) v.elementAt(files.length - i -1);
            int index = model.getIndexOfChild(node,here);
            if (index < 0) return;
            files[i] = node = model.getChild(node,index);
         }
         TreePath path = new TreePath(files);
         tree.setSelectionPath(path);
         tree.scrollPathToVisible(path);
      }
   }
   /**
    * Set the directory to which the tree is to open
    */
   public void setCurrentDirectory(File dir)
   {
      currentDirectory = dir;
   }
   /**
    * Set a filter to control which files are displayed in the tree
    */
   public void setFileFilter(FileFilter fileFilter)
   {
      foo.setFileFilter(fileFilter);
      model.changed();
   }
   /**
    * Get the current file filter
    * @return The current FileFilter or null if none set
    */
   public FileFilter getFileFilter()
   {
      return foo.getFileFilter();
   }
   /**
    * Select whether to show "hidden" files in the tree
    * @param hide True if hidden files should not be shown
    */
   public void setFileHidingEnabled(boolean hide)
   {
      foo.setFileHidingEnabled(hide);
      model.changed();
   }
   /**
    * Test if file hiding is enabled
    * @return true if file hiding is enabled
    * @see #setFileHidingEnabled
    */
   public boolean isFileHidingEnabled()
   {
      return foo.isFileHidingEnabled();
   }
   /**
    * Set the file selection mode. Valid modes are
    * @param mode either DIRECTORIES_ONLY or FILES_AND_DIRECTORIES (the default)
    * @see javax.swing.JFileChooser#setFileSelectionMode
    */
   public void setFileSelectionMode(int mode)
   {
      foo.setFileSelectionMode(mode);
      model.changed();
   }
   /**
    * Test the file selection mode
    * @return The current file selection mode
    * @see #setFileSelectionMode
    */
   public int getFileSelectionMode()
   {
      return foo.getFileSelectionMode();
   }
   /**
    * Sets the filechooser to allow multiple file selections.
    */
   public void setMultiSelectionEnabled(boolean enable)
   {
      foo.setMultiSelectionEnabled(enable);
      tree.getSelectionModel().setSelectionMode(enable ? TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION :
         TreeSelectionModel.SINGLE_TREE_SELECTION);
   }
   /**
    * Returns true if multi-file selection is enabled.
    */
   public boolean isMultiSelectionEnabled()
   {
      return foo.isMultiSelectionEnabled();
   }
   /**
    * Returns the currently selected file (or the first selected file if multiple files are selected)
    */
   public File getSelectedFile()
   {
      return (File) tree.getLastSelectedPathComponent();
   }
   /**
    * Returns a list of selected files if the filechooser is set to allow multi-selection.
    */
   public File[] getSelectedFiles()
   {
      TreePath[] paths =  tree.getSelectionPaths();
      File[] result = new File[paths.length];
      for (int i=0; i<paths.length; i++) result[i] = (File) paths[i].getLastPathComponent();
      return result;
   }
   protected void setup(FileSystemView view)
   {
      foo.setFileSystemView(view);
   }
   public void updateUI()
   {
      super.updateUI();
      foo.updateUI();
   }
   /**
    * Popup up a modal dialog containing the JDirectoryChooser
    * @param parent The parent of the dialog box
    * @return either CANCEL_OPTION or JAPPROVE_OPTION
    * @see javax.swing.JFileChooser#showDialog
    */
   public int showDialog(Component parent)
   {
      Frame frame = parent instanceof Frame ? (Frame) parent
      : (Frame)SwingUtilities.getAncestorOfClass(Frame.class, parent);
      
      String title = null;
      
      title = foo.getDialogTitle();
      if (title == null) foo.getUI().getDialogTitle(foo);
      
      returnValue = JFileChooser.CANCEL_OPTION;
      
      dialog = new JDialog(frame, title, true);
      dialog.getContentPane().add(this,BorderLayout.CENTER);
      
      dialog.pack();
      dialog.setLocationRelativeTo(parent);
      dialog.setVisible(true);
      
      return returnValue;
   }
   /**
    * Sets the string that goes in the FileChooser window's title bar
    */
   public void setDialogTitle(String dialogTitle)
   {
      foo.setDialogTitle(dialogTitle);
   }
   /**
    * Gets the string that goes in the FileChooser's titlebar
    */
   public String getDialogTitle()
   {
      return foo.getDialogTitle();
   }
   /**
    * Sets the file view to used to retrieve UI information, such as the icon that
    * represents a file or the type description of a file.
    */
   public void setFileView(FileView fileView)
   {
      foo.setFileView(fileView);
   }
   /**
    * Returns the current file view.
    */
   public FileView getFileView()
   {
      return foo.getFileView();
   }
   private FileTreeModel model = new FileTreeModel();
   private JFileChooser foo = new JFileChooser();
   private JButton approve = new JButton("Select");
   private JButton cancel = new JButton("Cancel");
   private File currentDirectory = null;
   private JTree tree;
   private JDialog dialog;
   private int returnValue;
   
   public static final int APPROVE_OPTION = JFileChooser.APPROVE_OPTION;
   public static final int CANCEL_OPTION = JFileChooser.CANCEL_OPTION;
   
   public static final int DIRECTORIES_ONLY = JFileChooser.DIRECTORIES_ONLY;
   public static final int FILES_AND_DIRECTORIES = JFileChooser.FILES_AND_DIRECTORIES;
   
   private static final boolean DIRS_FIRST = true;
 
   private static final Comparator comparator = new FileComparator();
   private static final Object root = new Object();
   
   private class ButtonListener implements ActionListener, TreeSelectionListener
   {
      public void actionPerformed(ActionEvent e)
      {
         if (e.getSource() == approve) returnValue = JFileChooser.APPROVE_OPTION;
         dialog.setVisible(false);
         dialog = null;
      }
      public void valueChanged(TreeSelectionEvent e)
      {
         approve.setEnabled(tree.getSelectionCount() > 0);
      }
   }
   
   private class FileRenderer extends DefaultTreeCellRenderer
   {
      public Component getTreeCellRendererComponent(JTree tree, Object node, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)
      {
         Component comp = super.getTreeCellRendererComponent(tree,node,selected,expanded,leaf,row,hasFocus);
         if (comp instanceof JLabel && node instanceof File)
         {
            JLabel label = (JLabel) comp;
            File f = (File) node;
            label.setText(foo.getName(f));
            label.setIcon(foo.getIcon(f));
         }
         return comp;
      }
   }
   private class FileTreeModel implements TreeModel
   {
      public Object getChild(Object node, int index)
      {
         return children(node)[index];
      }
      public int getChildCount(Object node)
      {
         return children(node).length;
      }
      public int getIndexOfChild(Object node, Object child)
      {
         File[] children = children(node);
         
         for (int i=0; i<children.length; i++)
            if (children[i].equals(child)) return i;
         
         return -1;
      }
      public Object getRoot()
      {
         return root;
      }
      public boolean isLeaf(Object node)
      {
         //return children(node).length == 0;
         return !(node == root || ((File) node).isDirectory());
      }
      public void valueForPathChanged(TreePath p1, Object p2)
      {
         // We dont allow model changes
      }
      /**
       * Adds a listener for the TreeModelEvent posted after the tree changes.
       *
       * @see     #removeTreeModelListener
       * @param   l       the listener to add
       */
      public void addTreeModelListener(TreeModelListener l)
      {
         listenerList.add(TreeModelListener.class, l);
      }
      
      /**
       * Removes a listener previously added with <B>addTreeModelListener()</B>.
       *
       * @see     #addTreeModelListener
       * @param   l       the listener to remove
       */
      public void removeTreeModelListener(TreeModelListener l)
      {
         listenerList.remove(TreeModelListener.class, l);
      }
      private File[] children(Object node)
      {
         for (int i=0; i<CACHE_SIZE; i++)
         {
            if (node == cachedNode[i]) return cachedChildren[i];
         }
         
         Window w = null;
         Cursor oldCursor = null;
         if (tree != null && tree.isVisible())
         {
            w = (Window) SwingUtilities.getAncestorOfClass(Window.class,tree);
            if (w != null)
            {
               oldCursor = w.getCursor();
               w.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
            }
         }
         try
         {
            FileSystemView view = foo.getFileSystemView();
            boolean isRoot = (node == root);
            File[] children = isRoot ? view.getRoots() : view.getFiles((File) node,foo.isFileHidingEnabled());
            if (!isRoot)
            {
               boolean dirOnly = foo.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY;
               if (foo.getFileFilter() != null || dirOnly)
               {
                  Vector v = new Vector();
                  for (int i=0; i<children.length; i++)
                  {
                     File f = children[i];
                     if (dirOnly && !f.isDirectory()) continue;
                     if (foo.accept(f)) v.addElement(children[i]);
                  }
                  if (v.size() != children.length)
                  {
                     children = new File[v.size()];
                     v.copyInto(children);
                  }
               }
            }
	    Arrays.sort(children,comparator);
            cachedNode[nextCache] = node;
            cachedChildren[nextCache] = children;
            nextCache = (nextCache + 1) % CACHE_SIZE;
            return children;
         }
         finally
         {
            if (w != null) w.setCursor(oldCursor);
         }
      }
      void changed()
      {
         cachedNode = new Object[CACHE_SIZE];
         cachedChildren = new File[CACHE_SIZE][];
         fireTreeStructureChanged(new TreeModelEvent(this, new TreePath(root)));
      }
      protected void fireTreeStructureChanged(TreeModelEvent e)
      {
         Object[] listeners = listenerList.getListenerList();
         // Process the listeners last to first, notifying
         // those that are interested in this event
         for (int i = listeners.length-2; i>=0; i-=2)
         {
            if (listeners[i]==TreeModelListener.class)
            {
               ((TreeModelListener)listeners[i+1]).treeStructureChanged(e);
            }
         }
      }
      private EventListenerList listenerList = new EventListenerList();
      private final int CACHE_SIZE = 10;
      private Object[] cachedNode = new Object[CACHE_SIZE];
      private File[][] cachedChildren = new File[CACHE_SIZE][];
      private int nextCache = 0;
   }
   
   private static class FileComparator implements Comparator
   {
      public int compare(Object o1, Object o2)
      {
      	File f1 = (File) o1;
	File f2 = (File) o2;
	if (DIRS_FIRST)
	{
	   boolean d1 = f1.isDirectory();
	   boolean d2 = f2.isDirectory();
	   if (d1 && !d2) return -1;
	   if (!d1 && d2) return 1;
	}
	return f1.getName().compareTo(f2.getName());
      }
   }
   // Test code
   public static void main(String argv[]) throws Exception
   {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      final JTextField tf = new JTextField(40);
      JButton b = new JButton("Show Browser")
      {
         public void fireActionPerformed(ActionEvent e)
         {
            JDirectoryChooser dlg = new JDirectoryChooser(tf.getText());
	    dlg.setFileHidingEnabled(false);
            dlg.showDialog(this);
            if (dlg.getSelectedFile() != null) tf.setText(dlg.getSelectedFile().toString());
         }
      };
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(b,BorderLayout.CENTER);
      frame.getContentPane().add(tf,BorderLayout.SOUTH);
      frame.pack();
      frame.setVisible(true);
   }
}
