/**
 *#########################################################################
 *
 * A component of the Gatherer application, part of the Greenstone digital
 * library suite from the New Zealand Digital Library Project at the
 * University of Waikato, New Zealand.
 *
 * Author: John Thompson, Greenstone Digital Library, University of Waikato
 *
 * Copyright (C) 1999 New Zealand Digital Library Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */
package org.greenstone.gatherer.file;

import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Dictionary;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.collection.CollectionTreeModel;
import org.greenstone.gatherer.collection.CollectionTreeNode;
import org.greenstone.gatherer.collection.FullCollectionTreeNode;
import org.greenstone.gatherer.gui.GProgressBar;
import org.greenstone.gatherer.gui.tree.DragTree;
import org.greenstone.gatherer.metadata.MetadataValue;
import org.greenstone.gatherer.metadata.MetadataXMLFileManager;
import org.greenstone.gatherer.util.DragComponent;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.SynchronizedTreeModelTools;
import org.greenstone.gatherer.util.Utility;

/** A threaded object which processes a queue of file actions such as copying and movement. It also handles updating the various trees involved so they are an accurate representation of the file system they are meant to match.
 * @author John Thompson, Greenstone Digital Library, University of Waikato
 * @version 2.3
 */
public class FileQueue
    extends Thread
{
    /** The size of the io buffer, in bytes. */
    static final private int BUFFER_SIZE = 1024;

    /** When someone requests the movement queue to be dumped this cancel flag is set to true. */
    private boolean cancel_action = false;
    /** The button which controls the stopping of the file queue. */
    private JButton stop_button = null;
    /** true if the user has selected yes to all from a file 'clash' dialog. */
    private boolean yes_to_all = false;
    /** A label explaining the current moving files status. */
    private JLabel file_status = null;
    /** A list containing a queue of waiting movement jobs. */
    private ArrayList queue = null;
    /** A progress bar which shows how many bytes, out of the total size of bytes, has been moved. */
    private GProgressBar progress = null;

    /** The objects listening for FileCopiedSuccess events. */
    private ArrayList file_copied_success_listeners = new ArrayList();
    
    /** Constructor.
     */
    public FileQueue() {
	DebugStream.println("FileQueue started.");
	this.queue = new ArrayList();
	file_status = new JLabel(Dictionary.get("FileActions.No_Activity"));
	progress = new GProgressBar();
	progress.setBackground(Configuration.getColor("coloring.collection_tree_background", false));
	progress.setForeground(Configuration.getColor("coloring.collection_tree_foreground", false));
	progress.setString(Dictionary.get("FileActions.No_Activity"));
	progress.setStringPainted(true);
    }

    /** There are tasks that depend on whether a file copy was succesful, so need to maintain listeners
     * and need to fire events on successful copy. */
    public void fireFileCopiedSuccessfully(File new_file) {
	// Send the event off to all the matching Listeners
	for (int i = 0; i < file_copied_success_listeners.size(); i++) {
	    ((FileCopiedSuccessListener) file_copied_success_listeners.get(i)).fileCopiedSuccessfully(new_file);
	}
    }
    public void addFileCopiedSuccessListener(FileCopiedSuccessListener listener) {
	file_copied_success_listeners.add(listener);
    }
    public void removeFileCopiedSuccessListener(FileCopiedSuccessListener listener) {
	file_copied_success_listeners.remove(listener);
    }
    
    /** Add a new job to the queue, specifiying as many arguments as is necessary to complete this type of job (ie delete needs no target information).
     * @param id A long id unique to all jobs created by a single action.
     * @param source The DragComponent source of this file, most likely a DragTree.
     * @param child The FileNode you wish to mode.
     * @param target The DragComponent to move the file to, again most likely a DragTree.
     * @param parent The files new FileNode parent within the target.
     * @param type The type of this movement as an int, either COPY or DELETE.
     */
    public void addJob(long id, DragComponent source, FileNode[] children, DragComponent target, FileNode parent, byte type)
    {
	// Queue the sub-job(s) (this may fail if we are asked to delete a read only file)
	for (int i = 0; i < children.length; i++) {
	    addJob(id, source, children[i], target, parent, type, -1);
	}
    }
    
    synchronized private void addJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type, int position) {
 	FileJob job = new FileJob(id, source, child, target, parent, type);
	DebugStream.println("Adding job: " + job);
 	if(position != -1 && position <= queue.size() + 1) {
 	    queue.add(position, job);
 	}
 	else {
	    queue.add(job);
 	}
 	notify();
    }

    /** Calculates the total deep file size of the selected file nodes.
     * @param files a FileNode[] of selected files
     * @return true if a cancel was signalled, false otherwise
     * @see org.greenstone.gatherer.file.FileManager.Task#run()
     */
    public boolean calculateSize(FileNode[] files)
    {
	Gatherer.invokeInEDT_replacesProceedInCurrThread("FileQueue.calculateSize()",
		 Gatherer.SYNC,
		 new Runnable() {
		     public void run() {
			 file_status.setText(Dictionary.get("FileActions.Calculating_Size"));
		     }
		 });
	progress.setString(Dictionary.get("FileActions.Calculating_Size"));

	// Calculate the total file size of all the selected file nodes
	Vector remaining = new Vector();
	for (int i = 0; !cancel_action && i < files.length; i++) {
	    remaining.add(files[i]);
	}
	while (!cancel_action && remaining.size() > 0) {
	    FileNode node = (FileNode) remaining.remove(0);
	    if (node.isLeaf()) {
		progress.addMaximum(node.getFile().length());
	    }
	    else {
		for (int i = 0; !cancel_action && i < node.getChildCount(); i++) {
		    remaining.add(node.getChildAt(i));
		}
	    }
	}

	// Now we return if calculation was cancelled so that the FileManagers Task can skip the addJob phase correctly.
	if (cancel_action) {
	    cancel_action = false; // reset
	    return true;
	}
	else {
	    return false;
	}
    }

    /** This method is called to cancel the job queue at the next available moment. */
    public void cancelAction() {
	cancel_action = true;
	clearJobs();
    }


    /** Format the given filename path string so that it is no longer than the given width. If it is wider replace starting directories with ...
     * @param key The key <strong>String</Strong> used to retrieve a phrase from the dictionary for this item.
     * @param raw The raw filename path <strong>String</strong>.
     * @param width The maximum width as an <i>int</i>.
     * @return A path <strong>String</strong> no longer than width.
     */
    String formatPath(String key, String raw, int width) // package access
    {
	JLabel label = new JLabel(Dictionary.get(key, raw));
	int position = -1;
	while(label.getPreferredSize().width > width && (position = raw.indexOf(File.separator)) != -1) {
	    raw = "..." + raw.substring(position + 1);
	    label.setText(Dictionary.get(key, raw));
	}
	if(raw.indexOf(File.separator) == -1 && raw.startsWith("...")) {
	    raw = raw.substring(3);
	}
	return raw;
    }


    /** Access to the file state label. */
    public JLabel getFileStatus() {
	return file_status;
    }

    /** Access to the progress bar. */
    public GProgressBar getProgressBar() {
	return progress;
    }


    synchronized private void addFileJob(long id, DragComponent source, FileNode child, DragComponent target, FileNode parent, byte type)
    {
	queue.add(new FileJob(id, source, child, target, parent, type));
	notify();
    }


    private void doEmptyDirectoryDelete(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	File source_directory = source_node.getFile();

	// If the directory isn't empty then this will fail
	if (source_directory.delete() == false) {
	    // The source directory couldn't be deleted, so give the user the option of continuing or cancelling
	    if (showErrorDialog(Dictionary.get("FileActions.Could_Not_Delete", source_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}

	// Remove the node from the model
	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
    }


    private void doDirectoryDelete(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	File source_directory = source_node.getFile();

	// The last thing we will do is delete this directory (which should be empty by then)
	addFileJob(file_job.ID(), file_job.source, source_node, null, null, FileJob.DELETE_EMPTY_DIRECTORY);

	// Add a new Delete job for each child of this directory (except metadata.xml files)
	source_node.refresh();
	for (int i = 0; i < source_node.size(); i++) {
	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
	    if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
	    }
	}

	// Treat metadata.xml files specially: delete them first
	for (int i = 0; i < source_node.size(); i++) {
	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
	    if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
		break;
	    }
	}
    }


    private void doDirectoryCopy(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	FileNode target_node = file_job.getDestination();

	File source_directory = source_node.getFile();
	File target_directory = new File(target_node.getFile(), source_directory.getName());

	// Check that the source directory doesn't contain the target directory (will create a cyclic loop)
	if (target_directory.getAbsolutePath().startsWith(source_directory.getAbsolutePath())) {
	    if (showErrorDialog(Dictionary.get("FileActions.Cyclic_Path", source_directory.getName())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}

	// The target directory shouldn't already exist
	if (target_directory.exists()) {
	    if (showErrorDialog(Dictionary.get("FileActions.Folder_Already_Exists", target_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	target_directory.mkdirs();

	// Create a node for the new directory in the collection tree
	FileSystemModel target_model = (FileSystemModel) file_job.target.getTreeModel();
        boolean is_full_collection_tree = true;
        if (target_model instanceof CollectionTreeModel) {
          is_full_collection_tree = false;
        }
        FileNode new_target_node;
        if (is_full_collection_tree) {
          new_target_node = new FullCollectionTreeNode(target_directory);       
        } else {
           new_target_node = new CollectionTreeNode(target_directory);
        }
	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
	new_target_node.setParent(target_node);

        if (!is_full_collection_tree) {
          // Copy the non-folder level metadata assigned to the original directory to the new directory
          ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_directory);
          MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
        }
	// Add a new Copy job for each child of this directory (except metadata.xml files in gather pane)
	source_node.refresh();
	for (int i = 0; i < source_node.size(); i++) {
	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
	    if (is_full_collection_tree || !child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
		addFileJob(file_job.ID(), file_job.source, child_file_node, file_job.target, new_target_node, FileJob.COPY);
	    }
	}
    }


    private void doDirectoryMove(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	FileNode target_node = file_job.getDestination();

	File source_directory = source_node.getFile();
	File target_directory = new File(target_node.getFile(), source_directory.getName());
	if (file_job.type == FileJob.RENAME) {
	    // This is the only difference between moves and renames
	    target_directory = target_node.getFile();
	    target_node = (FileNode) source_node.getParent();
	}

	// Check the target directory isn't the source directory
	if (target_directory.equals(source_directory)) {
	    DebugStream.println("Target directory is the source directory!");
	    return;
	}

	// The target directory shouldn't already exist
	if (target_directory.exists()) {
	    if (showErrorDialog(Dictionary.get("FileActions.Folder_Already_Exists", target_directory.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	target_directory.mkdirs();

	// Create a node for the new directory in the collection tree
	FileSystemModel target_model = (FileSystemModel) file_job.target.getTreeModel();
        boolean is_full_collection_tree = true;
        if (target_model instanceof CollectionTreeModel) {
          is_full_collection_tree = false;
        }
        FileNode new_target_node;
        if (is_full_collection_tree) {
          new_target_node = new FullCollectionTreeNode(target_directory);
        } else {
          new_target_node = new CollectionTreeNode(target_directory);
        }
	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
	new_target_node.setParent(target_node);

        if (!is_full_collection_tree) {
          // Move the folder level metadata assigned to the original directory to the new directory
          ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_directory);
          MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
          MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
        }
	// The last thing we will do is delete this directory
	addFileJob(file_job.ID(), file_job.source, source_node, null, null, FileJob.DELETE);

	// Treat metadata.xml files specially: delete them last
	source_node.refresh();
	for (int i = 0; i < source_node.size(); i++) {
	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
	    if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
		addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE);
		break;
	    }
	}

	// Add a new Move job for each child of this directory (except metadata.xml files)
	for (int i = 0; i < source_node.size(); i++) {
	    FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i);
	    if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) {
		addFileJob(file_job.ID(), file_job.source, child_file_node, file_job.target, new_target_node, FileJob.MOVE);
	    }
	}
    }


    private void doFileDelete(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	File source_file = source_node.getFile();

	// Almost all files will be deleted from the collection tree (exception: files in "Downloaded Files")
	if (source_node instanceof CollectionTreeNode) {
	    // If we're deleting a metadata.xml file we must unload it
	    boolean metadata_xml_file = source_file.getName().equals(StaticStrings.METADATA_XML);
	    if (metadata_xml_file) {
		MetadataXMLFileManager.unloadMetadataXMLFile(source_file);
	    }
	    // Otherwise remove any metadata assigned directly to the file
	    else {
		ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
		MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
	    }
	}

	// Delete the source file
	if (!Utility.delete(source_file)) {
	    // The source file couldn't be deleted, so give the user the option of continuing or cancelling
	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Deleted_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}

	// Remove the node from the model
	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
    }


    private void doFileCopy(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	FileNode target_node = file_job.getDestination();

	File source_file = source_node.getFile();
	File target_file = new File(target_node.getFile(), source_file.getName());

	// The target file shouldn't already exist -- if it does ask the user whether they want to overwrite
	boolean overwrite_file = false;
	if (target_file.exists()) {
	    int result = showOverwriteDialog(target_file.getName());
	    if (result == JOptionPane.NO_OPTION) {
		// Don't overwrite
		return;
	    }
	    if (result == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
		return;
	    }

	    overwrite_file = true;
	}

	// Copy the file
	try {
	    copyFile(source_file, target_file, true);
	    // let any listeners know that the copy wasn't cancelled and took place without error
	    fireFileCopiedSuccessfully(source_file);
	}
	catch (FileAlreadyExistsException exception) {
	    // This should not ever happen, since we've called copyFile with overwrite set
	    DebugStream.printStackTrace(exception);
	    return;
	}
	catch (FileNotFoundException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Found_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    // Refresh the source tree model
	    FileSystemModel source_model = file_job.source.getTreeModel();
	    source_model.refresh(new TreePath(((FileNode) file_job.getOrigin().getParent()).getPath()));
	    return;
	}
	catch (InsufficientSpaceException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(Dictionary.get("FileActions.Insufficient_Space_Message", exception.getMessage())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	catch (IOException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(exception.getMessage()) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	catch (ReadNotPermittedException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(Dictionary.get("FileActions.Read_Not_Permitted_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	catch (UnknownFileErrorException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(Dictionary.get("FileActions.Unknown_File_Error_Message")) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}
	catch (WriteNotPermittedException exception) {
	    DebugStream.printStackTrace(exception);
	    if (showErrorDialog(Dictionary.get("FileActions.Write_Not_Permitted_Message", target_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}

        //**
        FileNode new_target_node;
        if (target_node instanceof CollectionTreeNode) {
            new_target_node = new CollectionTreeNode(target_file);
        } else {
          new_target_node = new FullCollectionTreeNode(target_file);
        }
	if (overwrite_file == false) {
	    // Add the new node into the tree
	    FileSystemModel target_model = file_job.target.getTreeModel();
	    SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);
	}
        if (target_node instanceof CollectionTreeNode) {
          // It's only in hte Gather pane that we care about what files are added, for plugin suggestions and bringing metadata over
          Gatherer.c_man.fireFileAddedToCollection(target_file);
          
          // Copy the non-folder level metadata assigned to the original file to the new file
          if (file_job.type == FileJob.COPY) {
	    // do metadata too
	    ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_file);
	    MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
          }
        }
    }


    private void doFileMove(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	FileNode target_node = file_job.getDestination();

	File source_file = source_node.getFile();
	File target_file = new File(target_node.getFile(), source_file.getName());
	if (file_job.type == FileJob.RENAME) {
	    // This is the only difference between moves and renames
	    target_file = target_node.getFile();
	    target_node = (FileNode) source_node.getParent();
	}

	// Check the target file isn't the source file
	if (target_file.equals(source_file)) {
	    DebugStream.println("Target file is the source file!");
	    return;
	}

	// The target file shouldn't already exist
	if (target_file.exists()) {
	    int result = showOverwriteDialog(target_file.getName());
	    if (result == JOptionPane.NO_OPTION) {
		// Don't overwrite
		return;
	    }
	    if (result == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
		return;
	    }
	}

	// Move the file by renaming it
	if (!source_file.renameTo(target_file)) {
	    String args[] = { source_file.getName(), target_file.getAbsolutePath() };
	    if (showErrorDialog(Dictionary.get("FileActions.File_Move_Error_Message", args)) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    return;
	}

          
	// Remove the node from the source model and add it to the target model
	SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node);
        
        FileNode new_target_node;
        if (target_node instanceof CollectionTreeNode) {
          new_target_node = new CollectionTreeNode(target_file);
        } else {
          new_target_node = new FullCollectionTreeNode(target_file);
        }
	FileSystemModel target_model = file_job.target.getTreeModel();
	SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node);

        if (target_node instanceof CollectionTreeNode) {
          // Move the non-folder level metadata assigned to the original file to the new file
          ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
          MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
          MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
        }
    }

    
    /** all this does is move the metadata, and delete the source */
    private void doFileReplace(FileJob file_job)
    {
	FileNode source_node = file_job.getOrigin();
	FileNode target_node = file_job.getDestination();

	File source_file = source_node.getFile();
	File target_file = target_node.getFile();

	// Move the non-folder level metadata assigned to the original file to the new file
        if (target_node instanceof CollectionTreeNode) {
          CollectionTreeNode new_target_node = new CollectionTreeNode(target_file);
          ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file);
          MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata);
          MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata);
        }
	// now delete the original
	doFileDelete(file_job);
    }


    private void processFileJob(FileJob file_job)
    {
	DebugStream.println("Processing file job " + file_job + "...");

	// Ensure that the source file exists
	File source_file = file_job.getOrigin().getFile();
	if (!source_file.exists()) {
	    // The source file doesn't exist, so give the user the option of continuing or cancelling
	    if (showErrorDialog(Dictionary.get("FileActions.File_Not_Found_Message", source_file.getAbsolutePath())) == JOptionPane.CANCEL_OPTION) {
		clearJobs();  // Aborting action
	    }
	    // Refresh the source tree model
	    FileSystemModel source_model = file_job.source.getTreeModel();
	    source_model.refresh(new TreePath(((FileNode) file_job.getOrigin().getParent()).getPath()));
	    return;
	}

	// Enable the "Stop" button
	stop_button.setEnabled(true);

	// Delete empty directory job
	if (file_job.type == FileJob.DELETE_EMPTY_DIRECTORY) {
	    file_status.setText(Dictionary.get("FileActions.Deleting", formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width)));
	    doEmptyDirectoryDelete(file_job);
	    return;
	}

	// Delete job
	if (file_job.type == FileJob.DELETE) {
	    file_status.setText(Dictionary.get("FileActions.Deleting", formatPath("FileActions.Deleting", source_file.getAbsolutePath(), file_status.getSize().width)));
	    if (source_file.isFile()) {
		long source_file_size = source_file.length();
		doFileDelete(file_job);
		progress.addValue(source_file_size);  // Update progress bar
	    }
	    else {
		doDirectoryDelete(file_job);
	    }
	    return;
	}

	// Copy job
	if (file_job.type == FileJob.COPY || file_job.type == FileJob.COPY_FILE_ONLY) {
	    file_status.setText(Dictionary.get("FileActions.Copying", formatPath("FileActions.Copying", source_file.getAbsolutePath(), file_status.getSize().width)));
	    if (source_file.isFile()) {
		long source_file_size = source_file.length();
		doFileCopy(file_job);
		progress.addValue(source_file_size);  // Update progress bar
	    }
	    else {
		doDirectoryCopy(file_job);
	    }
	    return;
	}

	// Move (or rename) job
	if (file_job.type == FileJob.MOVE || file_job.type == FileJob.RENAME) {
	    file_status.setText(Dictionary.get("FileActions.Moving", formatPath("FileActions.Moving", source_file.getAbsolutePath(), file_status.getSize().width)));
	    if (source_file.isFile()) {
		long source_file_size = source_file.length();
		doFileMove(file_job);
		progress.addValue(source_file_size);  // Update progress bar
	    }
	    else {
		doDirectoryMove(file_job);
	    }
	    return;
	}

	// Replace job
	if (file_job.type == FileJob.REPLACE) {
	    file_status.setText(Dictionary.get("FileActions.Replacing", formatPath("FileActions.Replacing", source_file.getAbsolutePath(), file_status.getSize().width)));
	    doFileReplace(file_job);
	    return;
	}
    }


    private int showErrorDialog(String error_message)
    {
	Object[] options = { Dictionary.get("General.OK"), Dictionary.get("General.Cancel") };
	int result = JOptionPane.showOptionDialog(Gatherer.g_man, error_message, Dictionary.get("General.Error"), JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, options[0]);
	if (result == 0) {
	    return JOptionPane.OK_OPTION;
	}
	else {
	    return JOptionPane.CANCEL_OPTION;
	}
    }


    private int showOverwriteDialog(String target_file_name)
    {
	// Has "yes to all" been set?
	if (yes_to_all) {
	    return JOptionPane.YES_OPTION;
	}

	Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("FileActions.Yes_To_All"), Dictionary.get("General.No"), Dictionary.get("General.Cancel") };
	int result = JOptionPane.showOptionDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Exists", target_file_name), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
	if (result == 0) {
	    return JOptionPane.YES_OPTION;
	}
	else if (result == 1) {
	    yes_to_all = true;
	    return JOptionPane.YES_OPTION;
	}
	else if (result == 2) {
	    return JOptionPane.NO_OPTION;
	}
	else {
	    return JOptionPane.CANCEL_OPTION;
	}
    }


    public void run()
    {
	super.setName("FileQueue");

	while (!Gatherer.exit) {
	    // Retrieve the next job
	    int position = queue.size() - 1;
	    if (position >= 0) {
		// We have a file job, so process it
		//processFileJob((FileJob) queue.remove(position));

		// other threads coming to FileQueue thread here, must be told accurate value
		// of 'position', so do the remove(position) operation on FileQueue thread.
		// As usual: variable declared final so it can be accessed in inner class
		// Only outer class' member and final local vars are available to inner class
		
		// Dr Bainbridge approved the following invokeOnEDT after thinking on it too
		final FileJob currentJob = (FileJob) queue.remove(position);
		
		Gatherer.invokeInEDT_replacesProceedInCurrThread(
		 "FileQueue.run() - processFileJob",
		 Gatherer.SYNC,
		 new Runnable() {
		     public void run() {
			 processFileJob(currentJob);
		     }
		 });	
	    }
	    else {
		// No jobs, so reset and wait until we are notified of one
		synchronized(this) {

		    // Dr Bainbridge OKed the following rewrite, who also felt this was the way
		    Gatherer.invokeInEDT_replacesProceedInCurrThread(
		     "FileQueue.run() - processFileJob",
		     Gatherer.SYNC,
		     new Runnable() {
			 public void run() {
			     // Force both workspace and collection trees to refresh
			     if (Gatherer.g_man != null && Gatherer.c_man.ready()) {
				 Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED);
				 // Need this next line back in to refresh the collection tree after
				 // rightclick>Unzip has finished unzipping and deleting the original zip file
				 // TODO: But why was it commented out originally? Should it not be active?
				 Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED);
			     }
			     
			     // Reset status area
			     file_status.setText(Dictionary.get("FileActions.No_Activity"));
			     progress.reset();
			     progress.setString(Dictionary.get("FileActions.No_Activity"));
			 }
		     });
		    
		    // Reset "yes to all" and "cancel" flags
		    yes_to_all = false;
		    cancel_action = false;

		    // Wait for a new file job
		    try {
			wait();
		    }
		    catch (InterruptedException exception) {}
		}
	    }
	}
    }


    /** Register the button that will be responsible for stopping executing file actions. 
     * @param stop_button a JButton
     */
    public void registerStopButton(JButton stop_button) {
	this.stop_button = stop_button;
    }


    synchronized private void clearJobs() {
	queue.clear();
    }
    
    /** Copy the contents from the source directory to the destination 
     * directory.
     * @param source The source directory
     * @param destination The destination directory
     * @see org.greenstone.gatherer.Gatherer
     */
    public void copyDirectoryContents(File source, File destination, boolean overwrite) 
	throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException
    {
	if (!source.isDirectory()) return;
	// check that dest dirs exist
	destination.mkdirs();
	
	File [] src_files = source.listFiles();
	if (src_files.length == 0) return; // nothing to copy
	for (int i=0; i<src_files.length; i++) {
	    File f = src_files[i];
	    String f_name = f.getName();
	    File new_file = new File(destination, f_name);
	    if (f.isDirectory()) {
		copyDirectoryContents(f, new_file, overwrite);
	    } else if (f.isFile()) {
		copyFile(f, new_file, overwrite);
	    }
	}
    }

    /** Preserving old default behaviour of copyDirContents method, where overwrite is false. */
    public void copyDirectoryContents(File source, File destination) throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException 
    {
	copyDirectoryContents(source, destination, false);
    }

    
    /** Copy a file from the source location to the destination location.
     * @param source The source File.
     * @param destination The destination File.
     * @see org.greenstone.gatherer.Gatherer
     */
    public void copyFile(File source, File destination, boolean overwrite)
	throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException
    {
	if (source.isDirectory()) {
	    destination.mkdirs();
	    return;
	}

	// Check if the origin file exists.
	if (!source.exists()) {
	    DebugStream.println("Couldn't find the source file.");
	    throw new FileNotFoundException();
	}

	// Make sure the destination file does not exist.
	if (destination.exists() && !overwrite) {
	    throw new FileAlreadyExistsException();
	}

	// Open an input stream to the source file
	FileInputStream f_in = null;
	try {
	    f_in = new FileInputStream(source);
	}
	catch (FileNotFoundException exception) {
	    // A FileNotFoundException translates into a ReadNotPermittedException in this case
	    throw new ReadNotPermittedException(exception.toString());		
	}

	// Create an necessary directories for the target file
	File dirs = destination.getParentFile();
	dirs.mkdirs();

	// Open an output stream to the target file
	FileOutputStream f_out = null;
	try {
	    f_out = new FileOutputStream(destination);
	}
	catch (FileNotFoundException exception) {
	    // A FileNotFoundException translates into a WriteNotPermittedException in this case
	    f_in.close();
	    throw new WriteNotPermittedException(exception.toString());
	}

	// Copy the file
	byte data[] = new byte[BUFFER_SIZE];
	int data_size = 0;
	while ((data_size = f_in.read(data, 0, BUFFER_SIZE)) != -1 && !cancel_action) {
	    long destination_size = destination.length();
	    try {
		f_out.write(data, 0, data_size);
	    }
	    // If an IO exception occurs, we can do some maths to determine if the number of bytes written to the file was less than expected. If so we assume a InsufficientSpace exception. If not we just throw the exception again.
	    catch (IOException io_exception) {
		f_in.close();
		f_out.close();

		if (destination_size + (long) data_size > destination.length()) {
		    // Determine the difference (which I guess is in bytes).
		    long difference = (destination_size + (long) data_size) - destination.length();
		    // Transform that into a human readable string.
		    String message = Utility.formatFileLength(difference);
		    throw new InsufficientSpaceException(message);
		}
		else {
		    throw(io_exception);
		}
	    }
	}

	// Flush and close the streams to ensure all bytes are written.
	f_in.close();
	f_out.close();

	// We have now, in theory, produced an exact copy of the source file. Check this by comparing sizes.
	if(!destination.exists() || (!cancel_action && source.length() != destination.length())) {
	    throw new UnknownFileErrorException();
	}

	// If we were cancelled, ensure that none of the destination file exists.
	if (cancel_action) {
	    destination.delete();
	}
    }
}
