package org.greenstone.gatherer.file;

import java.io.*;
import java.util.ArrayList;
import java.util.Enumeration;
import javax.swing.*;
import javax.swing.filechooser.*;
import javax.swing.tree.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.metadata.FilenameEncoding;
import org.greenstone.gatherer.util.ArrayTools;

import org.greenstone.gatherer.Gatherer;

public abstract class FileNode
    implements MutableTreeNode
{
    protected boolean allows_children = true;
    protected ArrayList child_nodes = null;
    protected ArrayList child_nodes_unfiltered = null;
    protected File file = null;
    protected FileSystemModel model = null;
    protected MutableTreeNode parent = null;	

	protected String urlEncodedFileName = "";
    protected String urlEncodedFilePath = "";
	protected String filenameEncoding = "";
    /** The string that is displayed as the filename. Attempts to be in the correct encoding. */
    protected String displayFileName = null;
	

    public FileNode(File file)
    {
		this.file = file;

		if (file != null) {
			// Files cannot have children
		    if(file.isFile()) {
				// Cache this result to prevent unceasing missing disk messages being thrown if the
				// removable media was, um, removed after directory mapped
				this.allows_children = false;
		    }
		    filenameEncoding = "";
		    urlEncodedFilePath = FilenameEncoding.calcURLEncodedFilePath(file);
		    urlEncodedFileName = FilenameEncoding.calcURLEncodedFileName(urlEncodedFilePath);

		    // work out the display string (extra special processing for CollectionTreeNodes)
		    displayFileName = calcDisplayString();	    
		}
    }

    public String getURLEncodedFileName() { return urlEncodedFileName; }

    public String getURLEncodedFilePath() { return urlEncodedFilePath; }

    public String getFilenameEncoding() { return filenameEncoding; }


	/** This method returns a string representation of the filenodes in the tree, 
	* that can then be displayed in the tree. Overridden in subclass CollectionTreeNode.
	* Turn FilenameEncoding.DEBUGGING on to see URLEncoded filenames.
	*/
    protected String calcDisplayString() {	
		if(FilenameEncoding.DEBUGGING) {
			return getURLEncodedFileName();
		} else {
			return file.getName();
		}
    }

    /** Returns the children of the node as an Enumeration. */
    public Enumeration children()
    {
	return new FileEnumeration();
    }


    /** Returns true if the receiver allows children.  */
    public boolean getAllowsChildren()
    {
	return allows_children;
    }


    /** Returns the child TreeNode at index childIndex. */
    public TreeNode getChildAt(int index)
    {
	return (TreeNode) child_nodes.get(index);
    }


    /** Returns the number of children TreeNodes the receiver contains. */
    public int getChildCount()
    {
	map();

	// Use the number of (filtered) child nodes
	if (child_nodes != null) {
	    return child_nodes.size();
	}

	return 0;
    }


    /** Returns the index of node in the receivers children. */
    public int getIndex(TreeNode node)
    {
	if (child_nodes != null) {
	    return child_nodes.indexOf(node);
	}

	return -1;
    }


    /** Returns the parent TreeNode of the receiver. */
    public TreeNode getParent()
    {
	return parent;
    }


    /** Adds child to the receiver at index. */
    public void insert(MutableTreeNode child, int index)
    {
	DebugStream.println("Insert " + child + " in " + this + " at index " + index + " [Model: " + model + "]");
	if (child == null) {
	    return;
	}

	try {
	    FileNode child_node = (FileNode) child;
	    child_nodes.add(index, child_node);
	    child_node.model = model;
	    child_node.parent = this;
	}
	catch (Exception exception) {
	    DebugStream.printStackTrace(exception);
	}
    }


    /** Returns true if the receiver is a leaf. */
    public boolean isLeaf()
    {
	return (allows_children == false);
    }


    /** Removes the child at index from the receiver. */
    public void remove(int index)
    {
	if (index >= 0 && index < child_nodes.size()) {
	    child_nodes.remove(index);
	}
    }


    /** Removes node from the receiver. */
    public void remove(MutableTreeNode node)
    {
	remove(getIndex(node));
    }


    /** Removes the receiver from its parent. */
    public void removeFromParent()
    {
	parent.remove(this);
	parent = null;
    }


    /** Resets the user object of the receiver to object. */
    public void setUserObject(Object object) {
	try {
	    file = (File) object;
	}
	catch (Exception exception) {
	    DebugStream.printStackTrace(exception);
	}
    }


    // -------------------------------------------------------------------------------


    public void add(MutableTreeNode child)
    {
	insert(child, child_nodes.size());
    }


    protected abstract FileNode addChildNode(File file);


    /** Compare two FileNodes for equality. */
    public boolean equals(FileNode node)
    {
	if (node == null) {
	    // Definitely not a match
	    return false;
	}

	if (file != null) {
	    return file.equals(node.getFile());
	}
	else {
	    return toString().equals(node.toString());
	}
    }


    /** Retrieve the file node at the given index, regardless of filters set. */
    public FileNode getChildAtUnfiltered(int index)
    {
 	if (index >= 0 && index < size()) {
 	    return (FileNode) child_nodes_unfiltered.get(index);
 	}
 	return null;
    }


    public File getFile() {
	return file;
    }


    /** Retrieves the tree path from the root node to this node. */
    public TreeNode[] getPath() {
	int count = 0;
	TreeNode current = this;
	while(current != null) {
	    count++;
	    current = current.getParent();
	}
	TreeNode[] path = new TreeNode[count];
	current = this;
	while(current != null) {
	    path[count - 1] = current;
	    count--;
	    current = current.getParent();
	}
	return path;
    }


    public boolean isFileSystemRoot() {
	if (file != null) {
	    return FileSystemView.getFileSystemView().isFileSystemRoot(file);
	}
	else {
	    return false;
	}
    }


    /** Overridden if necessary by subclasses. */
    public boolean isInLoadedCollection()
    {
	return false;
    }


    /** Overridden if necessary by subclasses. */
    public boolean isReadOnly()
    {
	return false;
    }


    public void map()
    {
	// If this node has already been mapped, don't bother doing it again
	if (child_nodes != null) {
	    return;
	}
	child_nodes = new ArrayList();

	// General case, only map if children are allowed
	if (file != null && getAllowsChildren()) {
	    File[] files = file.listFiles();
	    if (files != null && files.length > 0) {
		// Sort the child files
		ArrayTools.sort(files);

		// Now add them to child_nodes_unfiltered
		child_nodes_unfiltered = new ArrayList();
		for (int i = 0; i < files.length; i++) {
		    FileNode child_node = this.addChildNode(files[i]);
		    child_nodes_unfiltered.add(child_node);
		}

		// Apply the filters set in the model
		FileFilter[] filters = model.getFilters();
		for (int i = 0; filters != null && i < filters.length; i++) {
		    files = ArrayTools.filter(files, filters[i].filter, filters[i].exclude);
		}

		// Add the files left after filtering to child_nodes
		for (int i = 0, j = 0; (i < child_nodes_unfiltered.size() && j < files.length); i++) {
		    // Use the FileNode object in child_nodes_unfiltered rather than creating another
		    FileNode file_node = (FileNode) child_nodes_unfiltered.get(i);
		    if (file_node.getFile().equals(files[j])) {
			child_nodes.add(file_node);
			j++;
		    }
		}
		
			// in case any filename encodings had gone stale, 
			// (recalculate these and) refresh the display name
			refreshDescendantEncodings();			
					
	    }
	    
	    //model.nodeStructureChanged(this);
	    Gatherer.invokeInEDT_replacesProceedInCurrThread("FileNode.map()", Gatherer.SYNC, new Runnable() {   
		    public void run() {
			model.nodeStructureChanged(FileNode.this);
		    }
		});
	}
    }


    public void refresh()
    {
  	unmap();
  	map();
    }

	// overridden in subclass CollectionTreeNode to reset and reencode display strings
    public void resetDescendantEncodings() {}
    public void refreshDescendantEncodings() {}
	
	
    public void setModel(FileSystemModel model) {
	this.model = model;
    }

    public void setParent(MutableTreeNode parent) {
	this.parent = parent;
    }


    /** Return the total number of child files for the file this node represents, irrespective of filters set. */
    public int size() {
	if (child_nodes_unfiltered != null) {
	    return child_nodes_unfiltered.size();
	}
	return 0;
    }


    public String toString()
    {
	if (isFileSystemRoot()) {
	    return file.getAbsolutePath();
	}
	else {
	    if(displayFileName == null) {    
		displayFileName = calcDisplayString();
	    }
	    return displayFileName; //return file.getName();	    
	}
    }


    /** Unmap this node's children. */
    public void unmap()
    {
	DebugStream.println("Unmapping " + this + "...");
	child_nodes_unfiltered = null;
	child_nodes = null;
    }


    private class FileEnumeration
	implements Enumeration
    {
	private int index = 0;

	/** Tests if this enumeration contains more elements. */
	public boolean hasMoreElements() {
	    if(child_nodes == null) { // special case needed handling
		return false;
	    }
	    return (index < child_nodes.size());
	}

	/** Returns the next element of this enumeration if this enumeration object has at least one more element to provide. */
	public Object nextElement() {
	    Object result = null;
	    if (index < child_nodes.size()) {
		result = child_nodes.get(index);
		index++;
	    }
	    return result;
	}
    }
}
