package org.greenstone.gatherer.file;

import java.io.File;
import java.util.*;
import java.util.regex.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.gui.tree.DragTree;


public class FileSystemModel
    extends DefaultTreeModel 
    implements TreeExpansionListener, TreeWillExpandListener
{
    private DragTree tree = null;
    private FileFilter current_filter = null;
    private FileFilter[] filters = null;
    /** The filters in place for any file system model. */
    private FileFilter[] default_filters = { new FileFilter("\\..*", true), new FileFilter("metadata\\.xml", true) };

    public FileSystemModel(FileNode root) {
	super(root);
	root.setModel(this);
	root.refresh();
    }

    public int getChildCount(Object parent) {
	return ((TreeNode)parent).getChildCount();
    }

    public FileFilter[] getFilters() {
	if(filters == null) {
	    if(current_filter != null) {
		filters = new FileFilter[default_filters.length + 1];
		filters[default_filters.length] = current_filter;
	    }
	    else {
		filters = new FileFilter[default_filters.length];
	    }
	    System.arraycopy(default_filters, 0, filters, 0, default_filters.length);
	}
	return filters;
    }
	 
    /** Retrieve the node denoted by the given tree path. Note that this isn't equivelent to saying path.lastPathComponent, as the references within the path may be stale. */
    public FileNode getNode(TreePath path) {
	FileNode last = (FileNode)path.getLastPathComponent();
	DebugStream.println("Last Path Component = " + last);
	return last;
	/*
	///atherer.println("**** getNode(" + path + ") ****");
	FileNode current = (FileNode)root;
	// Special case for the root node. Check the first path component is the root node.
	FileNode first_node = (FileNode)path.getPathComponent(0);
	if(current.equals(first_node)) {
	    DebugStream.println("First path component matches root node.");
	    // For each path with this tree path
	    for(int i = 1; current != null && i < path.getPathCount(); i++) {
		// Retrieve the stale path
		Object stale_object = path.getPathComponent(i);
		FileNode stale_node = null;
		if(stale_object instanceof FileNode) {
		    stale_node = (FileNode) stale_object;
		}
		DebugStream.print("Searching for '" + stale_object + "': ");
		// Locate the fresh node by searching current's children. Remember to ensure that current is mapped.
		boolean found = false;

		// First we search through the mapped children
		for(int j = 0; !found && j < current.getChildCount(); j++) {
		    FileNode child_node = (FileNode) current.getChildAt(j);
		    DebugStream.print(child_node + " ");
		    if((stale_node != null && stale_node.equals(child_node)) || stale_object.toString().equals(child_node.toString())) {
			found = true;
			current = child_node;
			DebugStream.println("Found!");
		    }
		    child_node = null;
		}
		// Failing that we search through all the children, including filtered files
		for(int j = 0; !found && j < current.size(); j++) {
		    FileNode child_node = (FileNode) current.getChildAtUnfiltered(j);
		    DebugStream.print(child_node + " ");
		    if((stale_node != null && stale_node.equals(child_node)) || stale_object.toString().equals(child_node.toString())) {
			found = true;
			current = child_node;
			DebugStream.println("Found!");
		    }
		    child_node = null;
		}
		// If no match is found, then set current to null and exit.
		if(!found) {
		    current = null;
		    DebugStream.println("Not Found!");
		}
		else {
		    DebugStream.println("Returning node: " + new TreePath(current.getPath()));
		}
		// Repeat as necessary
	    }
	}
	return current;
	*/
    }

    public void insertNodeInto(MutableTreeNode newChild, MutableTreeNode parent, int index) {
	///ystem.err.println("insertNodeInto(" + newChild + ", " + parent + ", " + index + ")");
	super.insertNodeInto(newChild, parent, index);
    }


    // parameter needs to be final to work with Runnable anonymous inner class
    public void refresh(final TreePath thePath)
    {
     Gatherer.invokeInEDT_replacesProceedInCurrThread(
      "FileSystemModel.refresh()",
      Gatherer.SYNC, //TODO: ASYNC or SYNC? Both work, so I chose SYNC as callers may expect
                     // tree to be refreshed at the end of calling this method
      new Runnable() {
       public void run() {
	// Can only refresh if the model is currently being displayed in a tree
	if (tree == null) {
	    return;
	}

	TreePath path = thePath; // thePath is final variable, path is modifiable copy
	// If no path is set, take the path to the root node (ie. update the whole tree)
	if (path == null) {
	    // System.err.println("\nFileSystemModel.refresh(entire tree).");
	    path = new TreePath(((FileNode) root).getPath());

	    // Make sure the root node is expanded
	    tree.expandPath(path);
	}
	// else {
	//     System.err.println("\nFileSystemModel.refresh(" + path + ").");
	// }

	// Record all the expanded paths under this node
	Enumeration old_expanded_paths_enumeration = tree.getExpandedDescendants(path);
	if (old_expanded_paths_enumeration == null) {
	    return;
	}

	// Map and unmap the node to refresh its contents
	FileNode node = (FileNode) path.getLastPathComponent();
	node.refresh();

	// Fire the appropriate event
	nodeStructureChanged(node);

	// Sort the old expanded paths by length, smallest first
	ArrayList old_expanded_paths_list = Collections.list(old_expanded_paths_enumeration);
	Collections.sort(old_expanded_paths_list, new TreePathComparator());

	// Restore each of the expanded paths
	for (int i = 0; i < old_expanded_paths_list.size(); i++) {
  	    TreePath old_expanded_path = (TreePath) old_expanded_paths_list.get(i);
	    // System.err.println("Expanded path: " + old_expanded_path);

	    // Build up the new path in the tree
	    TreePath current_path = new TreePath(path.getPath());
  	    FileNode current_node = node;

	    // Traverse the tree to find the node to expand (or find it no longer exists)
	    while (!current_path.toString().equals(old_expanded_path.toString())) {
		// System.err.println("Current path: " + current_path);

		FileNode old_expanded_node =
		    (FileNode) old_expanded_path.getPathComponent(current_path.getPathCount());
		// System.err.println("Looking for: " + old_expanded_node);

		// Find the child node that matches the next element in the path
		boolean found = false;
		for (int j = 0; j < current_node.getChildCount(); j++) {
		    FileNode child_node = (FileNode) current_node.getChildAt(j);
		    // System.err.println("Child node: " + child_node);
		    if (child_node.equals(old_expanded_node)) {
			// System.err.println("Found!");
			current_path = current_path.pathByAddingChild(child_node);
			current_node = child_node;
			found = true;
			break;
		    }
		}

		// The node was not found, so we cannot expand this path
		if (!found) {
		    // System.err.println("Not found...");
		    break;
		}
	    }

	    // If we have built up the correct path, expand it
	    if (current_path.toString().equals(old_expanded_path.toString())) {
		tree.expandPath(current_path);
	    }
	}
       } // end run (on EDT)
      }); // end invoke on EDT
    }


    // The file tree can be filtered, so check that the node to be removed is actually part of the model
    public void removeNodeFromParent(MutableTreeNode node)
    {
	TreeNode parent_node = ((FileNode) node).getParent();
	if (parent_node != null && parent_node.getIndex((FileNode) node) != -1) {
	    super.removeNodeFromParent(node);
	}
    }
    
    public void removeNodeFromRoot(MutableTreeNode node, MutableTreeNode root)
    {
	//for(TreeNode child :  root.children()) { // can't do this
	// as children() returns an Enumeration, not Iterable. See
	// https://stackoverflow.com/questions/1240077/why-cant-i-use-foreach-on-java-enumeration

	for(int i = 0; i < root.getChildCount(); i++) {
	    MutableTreeNode child = (MutableTreeNode)root.getChildAt(i);
	    
	    // WorkspaceNode node is not actually in the tree, but a temporary node
	    // in memory, so need to compare against actual nodes in the tree to find a match
	    if(child.toString().equals(node.toString())) {
		// For whatever reason, these 2 ways of removing the shortcut node from
		// its parent, the root node (ABS_ROOT), don't work
		//child.removeFromParent();
		//root.remove(i);
		// The usual way, see in removeNodeFromParent() above works,
		// now that we've identified the matching node (the node in the tree to be
		// removed that matches the node representing the shortcut to be removed)
		super.removeNodeFromParent(child);
		break;
	    }
	}
    }

    private class TreePathComparator
	implements Comparator {

	public int compare(Object o1, Object o2)
	{
	    return (((TreePath) o1).getPathCount() - ((TreePath) o2).getPathCount());
	}
    }


    public void setFilter(String pattern) {
  	if(pattern != null) {
  	    current_filter = new FileFilter(pattern, false);
  	}
  	else {
  	    current_filter = null;
  	}
  	filters = null;
    }

    public void setTree(DragTree tree) {
	this.tree = tree;
    }

    public String toString() {
	if(tree != null) {
	    return tree.toString();
	}
	return "FileSystemModel";
    }

    /** Called whenever an item in the tree has been collapsed. */
    public void treeCollapsed(TreeExpansionEvent event) {
	// Deallocate the affected nodes children. Don't need to do this in a swing worker, as the nodes children are currently not visable.
	TreePath path = event.getPath();
	FileNode node = (FileNode) path.getLastPathComponent();
	node.unmap();
	// Fire the appropriate event.
	nodeStructureChanged(node);
    }

    /** Called whenever an item in the tree has been expanded. */
    public void treeExpanded(TreeExpansionEvent event) {
    }

    /** Invoked whenever a node in the tree is about to be collapsed. */
    public void treeWillCollapse(TreeExpansionEvent event) 
	throws ExpandVetoException {
	// Veto the event if the user is attempting to collapse the root node (regardless of whether it is visible).
	TreePath path = event.getPath();
	if(path.getPathCount() == 1) {
	    throw new ExpandVetoException(event, "Cannot collapse root node!");
	}
    }


    /** Invoked whenever a node in the tree is about to be expanded. */
    public void treeWillExpand(TreeExpansionEvent event) 
	throws ExpandVetoException
    {
	// Set the wait cursor
	Gatherer.g_man.wait(true);

	// Allocate the children (don't need a swing worker since the node's children are currently not visible)
	TreePath path = event.getPath();
	FileNode node = (FileNode) path.getLastPathComponent();
	node.refresh();
	nodeStructureChanged(node);

	// Restore the cursor
	Gatherer.g_man.wait(false);
    }
}
