import java.io.*;
import java.util.*;
import javax.swing.tree.*;
import java.net.*;

/**
 * A simple file manager, that can turn a directory tree into a tree of nodes,
 * suitable to be used by a <code>JTree</code>.
 */
public class SimpleFileManager {


  /**
   * How deep to descend into a directory tree at most. This is a safety 
   * feature, guarding against loops created by symbolic links as well as
   * a performance tweak.
   */
  public static final int MAXDEPTH=10;
   
  /**
   * The root directory to which all pathes are relative
   */
  private File root;
  
  /**
   * Cache the directory tree
   */
  private DefaultMutableTreeNode dirCache;
  
  /**
   * A thread, that monitors the directory tree for changes.
   */
  private Thread monitorThread;
  
  /**
   * The <code>FileMonitor</code> of the 
   * <code>monitorThread</code>
   */
  private FileMonitor fileMonitor;
  
  /**
   * The <code>FileFilter</code>, stating which files will be included in the
   * tree.
   */
  private FileFilter filter;
  

  /**
   * Construct a new file manager
   * @param root The toplevel directory of the directory tree to manage
   * @param filter a <code>FileFilter</code> for filtering the directory tree.
   * @exception IOException if root is inaccessible.
   */
  public SimpleFileManager(File root, FileFilter filter) throws IOException {
    if (root==null) throw new NullPointerException();
    this.filter=filter;
    if (!root.exists() || !root.canRead()) {
      throw new IOException(root.getName());
    }
    this.root=root;
  }
  
  /**
   * Query the root of this manager
   * @param the root directory for this manager
   */
  public File getRoot() {
    return root;
  }
  
  /**
   * Construct a tree of nodes, that represents the currently
   * being monitored directory tree and can directly be used by
   * a <code>JTree</code>.
   * @return the top node of the tree or null if construction failed
   * for whatever reason.
   */
  public synchronized DefaultMutableTreeNode getDirectoryTree() {
    if (dirCache==null) {
      dirCache = descend(root,MAXDEPTH);
    }
    return dirCache;
  }
  
  /**
   * Causes the filemanager to throw away all internal caches and rescan
   * it's directory tree.
   */
  public synchronized void refresh() {
    dirCache=null;
  }
  
  /**
   * Helper function to recursively travel through the directory tree
   * @param dir the directory to descend into 
   * @param depth depth counter. Will bail out, when this reaches 0.
   */
  private DefaultMutableTreeNode descend(File dir, int depth) {
    DefaultMutableTreeNode ret = new DefaultMutableTreeNode(dir.getName());
    File[] lst = dir.listFiles();
    Arrays.sort(lst);
    
    for (int i=0;i<lst.length;i++) {
      if (depth>0 && lst[i].canRead()) {
        if (lst[i].isDirectory()) {
          DefaultMutableTreeNode tmp = descend(lst[i], depth-1);
          if (tmp!=null) {
            ret.add(tmp);
          }
        }
        else {
          if (filter.accept(lst[i])) {
            ret.add (new DefaultMutableTreeNode(lst[i].getName()));
          }
        }
      }
    }
    if (ret.getChildCount()==0) {
      ret = null;
    }
    return ret;
  }
  
  /**
   * Stop monitoring the directory tree.
   */
  public void stopMonitoring() {
    try {
      monitorThread.interrupt();
    }
    catch (Exception e) {
      e.printStackTrace();
    } 
  }
  
  /**
   * Start directory monitoring
   * @param r a runnable, that will be submitted to 
   * <code>EventQueue.InvokeAndWait()</code> once a change in the directory
   * tree is detected.
   */
  public void startMonitoring() {
    fileMonitor = new FileMonitor(root,this);
    monitorThread = new Thread(fileMonitor);
    monitorThread.start();
  }
  
  /**
   * Query the monitor thread of this file manager
   * @return the thread, that is responsible for keeping
   * the filemanager in sync with the directory tree. Or null
   * if we are not monitoring currently.
   */
  public FileMonitor getFileMonitor() {
    return fileMonitor;
  }
  
  /**
   * Get the URI for a node
   * @param node a node created by this manager
   * @return the URI of the file, corresponding to the node, relative to
   * the root directory, this manager manages.
   */
  public URI getRelativePath(DefaultMutableTreeNode node) {
    File f = root.getParentFile();
    TreeNode[] tmp = node.getPath();
    for (int i=0;i<tmp.length;i++) {
      f = new File(f,tmp[i].toString());
    }
    URI base = root.toURI();
    return base.relativize(f.toURI());
  }
  
  /**
   * Find a file
   * @param u a relative URI (relative to this managers root directory).
   * @return the corresponsing file.
   */
  public File getFile(URI u) {
    File f = new File(root.toURI().resolve(u));
    // NOTE: URI.resolve() kills things like "../../..", so we should be safe
    // from a malicious scene hopping around in the file system.
    return f;
  }
  
  /**
   * Break a relative URI into components
   * @param u An URI as produced by <code>getRelativePath()</code>
   * @return An array of path components, including the root directory 
   * as the first one.
   */
  public String[] getComponents(URI u) {
    if (u==null) return new String[0];
    String tmp[] = u.getPath().split("/");
    String ret[] = new String[tmp.length+1];
    ret[0] = root.getName();
    System.arraycopy(tmp,0,ret,1,tmp.length);
    return ret;
  }
   
}
