/*
 * (C) 2004 - Geotechnical Software Services
 * 
 * This code is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This code is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this program; if not, write to the Free 
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 
 * MA  02111-1307, USA.
 */
package no.geosoft.cc.directory;



import java.util.*;
import java.io.File;
import java.text.Collator;

import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeModelEvent;

import javax.swing.*; // For testing only



/**
 * A TreeModel implementation for a disk directory structure.
 *
 * Typical usage:
 *
 *   <pre>
 *   FileSystemModel model = new FileSystemModel (new File ("/"));
 *   JTree tree = new JTree (model);
 *   </pre>
 * 
 * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
 */
public class FileSystemModel implements TreeModel
{
  private Collection    listeners_;
  private FileTreeNode  root_;
  private HashMap       sortedChildren_; // File -> List<File>
  private HashMap       lastModified_;


  
  /**
   * Create a tree model using the specified file as root.
   * 
   * @param root  Root file (directory typically).
   */
  public FileSystemModel (File root)
  {
    root_ = new FileTreeNode(root);

    listeners_      = new ArrayList();
    sortedChildren_ = new HashMap();
    lastModified_   = new HashMap();
  }


  
  public Object getRoot()
  {
    return root_;
  }


  public Object getChild (Object parent, int index)
  {
    List children = (List) sortedChildren_.get (parent);
    return children == null ? null : children.get (index);
  }


  
  public int getChildCount (Object parent)
  {
    File file = (File) parent;
    if (!file.isDirectory())
      return 0;

    File[] children = file.listFiles();
    int nChildren = children == null ? 0 : children.length;

    long lastModified = file.lastModified();
    
    boolean isFirstTime = lastModified_.get (file) == null;
    boolean isChanged = false;
    
    if (!isFirstTime) {
      Long modified = (Long) lastModified_.get (file);
      long diff = Math.abs (modified.longValue() - lastModified);
      isChanged = diff > 4000; // MS/Win or Samba HACK. Check this!
    }

    // Sort and register children info
    if (isFirstTime || isChanged) {
      lastModified_.put (file, new Long (lastModified));

      TreeSet sorted = new TreeSet();
      for (int i = 0; i < nChildren; i++)
        sorted.add (new FileTreeNode (children[i]));

      sortedChildren_.put (file, new ArrayList (sorted));
    }

    // Notify listeners (visual tree typically) if changes
    if (isChanged) {
      TreeModelEvent event = new TreeModelEvent (this, getTreePath (file));
      fireTreeStructureChanged (event);
    }

    return nChildren;
  }


  
  private Object[] getTreePath (File file)
  {
    List path = new ArrayList();
    while (!file.equals(root_)) {
      path.add(file);
      file = file.getParentFile();
    }
    path.add(root_);

    int nElements = path.size();

    Object[] treePath = new Object[nElements];
    for (int i = 0; i < nElements; i++)
      treePath[i] = path.get(nElements - i - 1);

    return treePath;
  }


  
  public boolean isLeaf (Object node)
  {
    return ((File) node).isFile();
  }


  
  public void valueForPathChanged (TreePath path, Object newValue)
  {
  }

  
  public int getIndexOfChild (Object parent, Object child)
  {
    List children = (List) sortedChildren_.get(parent);
    return children.indexOf (child);
  }

  
  public void addTreeModelListener (TreeModelListener listener)
  {
    if (listener != null && !listeners_.contains(listener))
      listeners_.add (listener);
  }

  
  public void removeTreeModelListener (TreeModelListener listener)
  {
    if (listener != null)
      listeners_.remove (listener);
  }

  
  public void fireTreeNodesChanged (TreeModelEvent event)
  {
    for (Iterator i = listeners_.iterator(); i.hasNext();) {
      TreeModelListener listener = (TreeModelListener) i.next();
      listener.treeNodesChanged (event);
    }
  }

  
  public void fireTreeNodesInserted (TreeModelEvent event)
  {
    for (Iterator i = listeners_.iterator(); i.hasNext();) {
      TreeModelListener listener = (TreeModelListener) i.next();
      listener.treeNodesInserted (event);
    }
  }

  
  public void fireTreeNodesRemoved (TreeModelEvent event)
  {
    for (Iterator i = listeners_.iterator(); i.hasNext();) {
      TreeModelListener listener = (TreeModelListener) i.next();
      listener.treeNodesRemoved (event);
    }
  }

  
  public void fireTreeStructureChanged (TreeModelEvent event)
  {
    for (Iterator i = listeners_.iterator(); i.hasNext();) {
      TreeModelListener listener = (TreeModelListener) i.next();
      listener.treeStructureChanged (event);
    }
  }


  
  /**
   * Extension to the java.io.File object but with more
   * appropriate compare rules
   * 
   * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
   */
  private class FileTreeNode extends File
    implements Comparable
  {
    public FileTreeNode(File file)
    {
      super(file, "");
    }

    
    /**
     * Compare two FileTreeNode objects so that directories
     * are sorted first.
     * 
     * @param object  Object to compare to.
     * @return        Compare identifier.
     */
    public int compareTo (Object object)
    {
      File file1 = this;
      File file2 = (File) object;

      Collator collator = Collator.getInstance();

      if (file1.isDirectory() && file2.isFile())
        return -1;
      else if (file1.isFile() && file2.isDirectory())
        return +1;
      else
        return collator.compare(file1.getName(), file2.getName());
    }


    
    /**
     * Retur a string representation of this node.
     * The inherited toString() method returns the entire path.
     * For use in a tree structure, the name is more appropriate.
     * 
     * @return  String representation of this node.
     */
    public String toString()
    {
      return getName();
    }
  }



  /**
   * Testing this class.
   * 
   * @param args  Not used
   */
  public static void main (String args[])
  {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);

    // The entire Linux root partition
    FileSystemModel model = new FileSystemModel (new File ("/"));
    JTree tree = new JTree (model);
    f.getContentPane().add (new JScrollPane (tree));
    
    f.pack();
    f.setVisible (true);
  }
}